APIドキュメントに別れを:tRPCでエンドツーエンドの型安全なアプリケーションを構築する
1. フロントエンドとバックエンドをつなぐ「契約」の難しさ
従来のフロントエンドとバックエンドを分離した開発では、APIが両者の「契約」となる。通常は次の方法でこの契約を管理する:
- RESTful API:OpenAPI(Swagger)などのツールを使い、詳しいAPIドキュメントを生成、管理する。
- GraphQL:厳密なSchema Definition Language(SDL)を使い、データ構造と操作を定義する。
これらの方法は有効だが、共通の問題がある。契約と実装が分離していることだ。フロントエンド開発者がAPIを呼び出すとき、そのAPIがドキュメントやスキーマに書かれたデータ構造を返すと信じる。しかしバックエンドの実装が変わり、たとえばフィールド名が変更されたのに、ドキュメントやスキーマが間に合って更新されなければ、その不一致は実行時になって初めて表面化し、バグを引き起こす。
この「契約」を実装と自動的に同期させ、さらにはコンパイル時に不一致を発見する方法はないだろうか?
2. tRPCの中心思想:スキーマではなく型を共有する
tRPC(TypeScript Remote Procedure Call)は、抜本的だが非常に単純な方法を提案する。フロントエンドとバックエンドの両方でTypeScriptを使うなら、型をそのまま共有すればよいのではないか?
tRPCでは、純粋なTypeScript関数をバックエンドAPIとして記述し、フロントエンドから直接呼び出せる。ローカルモジュールの関数を呼ぶように、完全な型推論と自動補完を利用できる。
スキーマにもコード生成にも依存しない。唯一の「契約」はTypeScriptの型そのものである。
3. どのように動くのか?
tRPCの魔法は、型推論と少し巧妙なカプセル化にある。
a. バックエンド:API Routerを定義する
バックエンド(通常はNode.jsサービス)では、tRPCの関数を使って一つ以上の「Router」を作成する。各Routerは、呼び出し可能な「Procedure」、つまりAPIエンドポイントの集合である。
// server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod'; // 使用 Zod 进行运行时校验
const t = initTRPC.create();
export const appRouter = t.router({
// 定义一个名为 `getUser` 的查询 procedure
getUser: t.procedure
.input(z.object({ userId: z.string() }))
.query(({ input }) => {
// 在这里查询数据库或执行其他逻辑
const user = { id: input.userId, name: 'Alex' };
return user;
}),
// 定义一个名为 `createUser` 的变更 procedure
createUser: t.procedure
.input(z.object({ name: z.string() }))
.mutation(({ input }) => {
const user = { id: `${Math.random()}`, name: input.name };
return user;
}),
});
// 导出 router 的类型定义
export type AppRouter = typeof appRouter;b. フロントエンド:クライアントを作成してAPIを呼び出す
フロントエンドでは、バックエンドからAppRouterという型だけをインポートすればよい。サーバー側のコードではなく、型だけをインポートしている点が重要だ。
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router'; // 只导入类型
export const trpc = createTRPCReact<AppRouter>();これでReactコンポーネントから、ローカル関数のようにAPIを呼び出し、完全な型安全性と自動補完を利用できる。
// client/components/UserInfo.tsx
import { trpc } from '../trpc';
function UserInfo({ userId }: { userId: string }) {
// `useQuery` 的第一个参数是 procedure 的路径
// 你输入 `trpc.` 时,IDE 会自动提示 `getUser` 和 `createUser`
const userQuery = trpc.getUser.useQuery({ userId });
if (userQuery.isLoading) {
return <div>Loading...</div>;
}
// `userQuery.data` 的类型被自动推断为 { id: string; name: string }
return <div>User: {userQuery.data?.name}</div>;
}この時点でバックエンド開発者が、getUserの返すnameフィールドをfullNameに変更すると、フロントエンドのuserQuery.data?.nameは実行時を待たず、TypeScriptのコンパイル時にすぐエラーになる。
4. tRPCを選ぶ理由
- 完全なエンドツーエンド型安全性:これが中心的な価値である。API契約の不一致によって発生する一群のバグを取り除ける。
- 優れた開発者体験:IDEの自動補完により、ドキュメントを調べたりAPI構造を推測したりする必要がない。リファクタリングも非常に簡単かつ安全になる。
- コード生成が不要:追加のビルド工程がなく、フィードバックループが極めて速い。
- 軽量で柔軟:tRPC自体は非常に小さく、どのフロントエンドフレームワークやバックエンドサービスとも連携できる。
結論
tRPCは、フルスタックTypeScript開発にこれまでにない滑らかな体験をもたらす。APIドキュメントとスキーマへの依存をなくすことで、フロントエンドとバックエンドの連携をシームレスかつ非常に安全にする。モノレポ構成では、その長所がさらに際立つ。フルスタックTypeScriptアプリケーションを構築しているなら、tRPCは時間をかけて試す価値のある画期的なツールだ。