APIドキュメントに別れを:tRPCでエンドツーエンドの型安全なアプリケーションを構築する

6 分

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は時間をかけて試す価値のある画期的なツールだ。