T-CREATOR

tRPC 使い方入門:Todo API を 50 行で作るフルスタック体験

tRPC 使い方入門:Todo API を 50 行で作るフルスタック体験

Web API を開発するとき、「サーバーで定義した型がクライアントで使えたら…」と思ったことはありませんか?tRPC を使えば、TypeScript の型がサーバーからクライアントへ自動で共有され、わずか 50 行程度のコードで型安全な Todo API を作れます。

本記事では、tRPC を使ったフルスタック開発の基本を、実際に動く Todo API を作りながら体験していきましょう。REST API や GraphQL と比べて圧倒的に少ないコード量で、型安全性を保ったまま開発できる感動を味わえますよ。

背景

tRPC とは何か

tRPC(TypeScript Remote Procedure Call)は、TypeScript 専用の型安全な API 通信ライブラリです。サーバー側で定義した関数の型情報が、ビルド時にクライアント側へ自動的に伝わるため、API の入出力を TypeScript の型システムで完全に保護できます。

従来の Web API 開発では、サーバーとクライアントで型定義を二重管理する必要がありました。しかし tRPC を使えば、型定義はサーバー側だけで完結し、クライアントは自動補完とエラーチェックの恩恵を受けられるのです。

以下の図は、tRPC がサーバーとクライアント間でどのように型情報を共有するかを示しています。

mermaidflowchart LR
  server["Server<br/>(tRPC Router)"] -->|型情報を<br/>自動生成| client["Client<br/>(React Component)"]
  client -->|型安全な<br/>関数呼び出し| server
  server -->|型付き<br/>レスポンス| client

  style server fill:#e1f5ff
  style client fill:#fff4e1

図で理解できる要点:

  • サーバー側で定義した tRPC Router の型が自動的にクライアントへ伝わる
  • クライアントは通常の関数を呼ぶ感覚で型安全な API 通信ができる
  • レスポンスも型付きで返ってくるため、実行時エラーを大幅に削減可能

tRPC が注目される理由

フルスタック TypeScript 開発の人気が高まる中、tRPC は以下の特徴で注目を集めています。

#特徴説明
1型の自動共有サーバーの型定義がクライアントで即座に使える
2ゼロコード生成GraphQL のようなスキーマファイルや型生成スクリプトが不要
3軽量追加の DSL や複雑な設定なしで導入可能
4React Query 統合データフェッチング、キャッシュ、リフェッチを簡単に実装
5開発体験の向上エディタの自動補完で API 仕様を把握しながら開発できる

特に、Next.js や Remix などのフルスタックフレームワークとの相性が抜群で、サーバーとクライアントを同じリポジトリで管理するモノレポ構成で真価を発揮します。

課題

REST API における型安全性の問題

REST API を使った開発では、以下のような型の問題が頻繁に発生します。

typescript// サーバー側 (例: Express + TypeScript)
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

app.get('/api/todos', (req, res) => {
  const todos: Todo[] = getTodos();
  res.json(todos);
});
typescript// クライアント側 (例: React)
// サーバーの Todo 型を知らないため、型定義を再現する必要がある
interface Todo {
  id: number;
  title: string;
  completed: boolean; // タイポや型の不一致が起こりやすい
}

const response = await fetch('/api/todos');
const todos: Todo[] = await response.json(); // 型アサーションが必要

上記のコードには以下の課題があります。

主な課題:

  1. 型定義の二重管理 - サーバーとクライアントで同じ型を別々に定義しなければならない
  2. 型の不一致 - サーバー側で型が変更されてもクライアント側は気づけない
  3. 実行時エラー - ビルド時に型チェックができず、実行時に初めてエラーが発覚する
  4. 手動の型アサーション - as や型注釈で強制的に型をつける必要がある

GraphQL の学習コストと複雑性

GraphQL は型安全性を提供しますが、以下のような導入コストがかかります。

#課題詳細
1スキーマ定義SDL (Schema Definition Language) を別途学習・記述する必要がある
2コード生成graphql-codegen などのツールで型を生成する設定が必要
3リゾルバー実装スキーマとリゾルバーを別々に管理しなければならない
4ビルドプロセススキーマから型を生成するステップがビルドに追加される
5学習曲線Query / Mutation / Subscription の概念を理解する必要がある

小規模なプロジェクトや API が限定的な場合、GraphQL は過剰な仕組みになってしまうことも少なくありません。

以下の図は、従来の API 開発における型の流れと課題を示しています。

mermaidflowchart TD
  serverDef["サーバー側<br/>型定義"] -->|手動で<br/>コピー| clientDef["クライアント側<br/>型定義"]
  serverDef -->|API実装| api["REST/GraphQL<br/>エンドポイント"]
  api -->|レスポンス| client["クライアント"]
  clientDef -.->|型アサーション| client

  serverDef -.->|変更| change["型変更"]
  change -.->|同期忘れ| clientDef
  change -->|実行時<br/>エラー| runtime["Runtime Error"]

  style runtime fill:#ffe1e1
  style change fill:#fff4e1

図で理解できる要点:

  • サーバーとクライアントで型定義が分離しているため、手動同期が必要
  • サーバー側で型を変更してもクライアント側に自動で伝わらない
  • 型の不一致は実行時エラーとして発覚し、開発効率が低下する

解決策

tRPC が実現する型の自動共有

tRPC は TypeScript の型推論を活用 して、サーバーとクライアント間で型を自動共有します。サーバー側で tRPC Router を定義すると、その型情報がクライアント側の tRPC Client に自動的に伝わるのです。

typescript// サーバー側で定義
export const appRouter = router({
  getTodos: publicProcedure.query(() => {
    return [
      { id: 1, title: 'Learn tRPC', completed: false },
    ];
  }),
});

export type AppRouter = typeof appRouter;
typescript// クライアント側で使用
import type { AppRouter } from './server';

const trpc = createTRPCClient<AppRouter>({...});

// getTodos の戻り値の型が自動的に推論される
const todos = await trpc.getTodos.query();
// todos の型: { id: number; title: string; completed: boolean }[]

このように、サーバーの Router 型をクライアントにインポートするだけで、API の入出力が完全に型安全になります。

tRPC の仕組み

tRPC の型共有は、以下の 3 つの要素で成り立っています。

#要素役割
1Routerサーバー側で API エンドポイント(Procedure)を定義する
2ProcedureQuery(読み取り)や Mutation(書き込み)の処理ロジックを記述
3Clientサーバーの Router 型を受け取り、型安全な関数として API を呼び出す

Router と Client を型でつなぐことで、型定義ファイルやスキーマファイルを一切書かずに、TypeScript の型システムだけで型安全性を実現しています。

以下の図は、tRPC における型の流れを示しています。

mermaidsequenceDiagram
  participant Dev as 開発者
  participant Server as tRPC Router
  participant TS as TypeScript<br/>Compiler
  participant Client as tRPC Client

  Dev->>Server: Procedure を定義
  Server->>TS: AppRouter 型を生成
  TS->>Client: 型情報を伝達
  Client->>Dev: 自動補完・型チェック
  Dev->>Client: query/mutation 呼び出し
  Client->>Server: HTTP リクエスト
  Server->>Client: 型付きレスポンス

図で理解できる要点:

  • 開発者が Procedure を定義すると、TypeScript が型情報を自動生成
  • 型情報はビルド時にクライアントへ伝わり、エディタで自動補完が効く
  • 実行時は通常の HTTP 通信だが、型安全性が保証される

REST API と tRPC の比較

tRPC を使うと、コード量とメンテナンスコストが大幅に削減されます。

#項目REST APItRPC
1型定義サーバー・クライアント両方で必要サーバー側のみ
2型の同期手動(ドキュメントや定義ファイル)自動(TypeScript の型推論)
3エンドポイント管理URL 文字列で管理関数名で管理(タイポ防止)
4エラーハンドリングHTTP ステータスを手動チェック型安全な Error オブジェクト
5開発体験Postman などで API 仕様確認エディタの自動補完で即座に確認

REST API では URL やリクエストボディを文字列で扱うため、タイポや型の不一致が実行時まで検出されません。一方 tRPC は、関数呼び出しの形で API を扱うため、ビルド時に全ての型エラーをキャッチできるのです。

具体例

プロジェクトのセットアップ

まず、Next.js プロジェクトを作成し、tRPC の必要なパッケージをインストールします。

bash# Next.js プロジェクトを作成
yarn create next-app trpc-todo-app --typescript
cd trpc-todo-app
bash# tRPC 関連パッケージをインストール
yarn add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod

上記のコマンドで、tRPC のサーバー・クライアント機能と、データフェッチング用の React Query、バリデーション用の Zod がインストールされます。

サーバー側の実装

1. tRPC Router の初期化

tRPC の基本設定として、Router と Procedure を作成するヘルパー関数を定義します。

typescript// src/server/trpc.ts
import { initTRPC } from '@trpc/server';

// tRPC インスタンスを初期化
const t = initTRPC.create();

// Router と Procedure を作成するヘルパーをエクスポート
export const router = t.router;
export const publicProcedure = t.procedure;

initTRPC.create() で tRPC インスタンスを作成し、routerpublicProcedure という 2 つのヘルパー関数を定義しています。これらを使って API エンドポイントを構築していきます。

2. Todo の型定義とバリデーション

Zod を使って、Todo データのバリデーションスキーマを定義します。

typescript// src/server/router.ts
import { z } from 'zod';
import { router, publicProcedure } from './trpc';

// Todo のバリデーションスキーマ
const todoSchema = z.object({
  id: z.number(),
  title: z.string(),
  completed: z.boolean(),
});

// インメモリで Todo を管理(実際は DB を使用)
let todos = [
  { id: 1, title: 'Learn tRPC', completed: false },
  { id: 2, title: 'Build Todo API', completed: false },
];

Zod のスキーマ定義により、実行時のバリデーションと TypeScript の型推論を同時に実現できます。todoSchema から自動的に型が推論されるため、型定義を別途書く必要はありません。

3. CRUD 操作の Procedure 定義

Todo の一覧取得、追加、更新、削除の 4 つの Procedure を定義します。

typescript// src/server/router.ts(続き)

export const appRouter = router({
  // Todo 一覧を取得
  getTodos: publicProcedure.query(() => {
    return todos;
  }),

  // Todo を追加
  addTodo: publicProcedure
    .input(z.object({ title: z.string() }))
    .mutation(({ input }) => {
      const newTodo = {
        id: todos.length + 1,
        title: input.title,
        completed: false,
      };
      todos.push(newTodo);
      return newTodo;
    }),

上記では、getTodos という Query と addTodo という Mutation を定義しています。input() メソッドで入力値のバリデーションを行い、型安全性を確保しています。

typescript  // Todo を更新
  updateTodo: publicProcedure
    .input(z.object({ id: z.number(), completed: z.boolean() }))
    .mutation(({ input }) => {
      const todo = todos.find(t => t.id === input.id);
      if (todo) {
        todo.completed = input.completed;
      }
      return todo;
    }),

  // Todo を削除
  deleteTodo: publicProcedure
    .input(z.object({ id: z.number() }))
    .mutation(({ input }) => {
      todos = todos.filter(t => t.id !== input.id);
      return { success: true };
    }),
});

// Router の型をエクスポート(クライアント側で使用)
export type AppRouter = typeof appRouter;

updateTododeleteTodo も同様に Mutation として定義します。最後に AppRouter 型をエクスポートすることで、クライアント側で型安全な API 呼び出しが可能になります。

4. Next.js API Route の設定

Next.js の API Route で tRPC を動かすための設定を行います。

typescript// src/pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/router';

// Next.js API Route として tRPC を公開
export default createNextApiHandler({
  router: appRouter,
  createContext: () => ({}), // 認証情報などを渡す場合はここで設定
});

createNextApiHandler を使うことで、tRPC Router を ​/​api​/​trpc​/​* というエンドポイントとして公開できます。

クライアント側の実装

1. tRPC Client の初期化

サーバーの型情報を受け取る tRPC Client を設定します。

typescript// src/utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';

// AppRouter 型を使って tRPC Client を作成
export const trpc = createTRPCReact<AppRouter>();

createTRPCReact<AppRouter>() により、サーバーの AppRouter 型がクライアントに伝わり、型安全な API 呼び出しが可能になります。

2. React Query Provider の設定

tRPC は内部で React Query を使用するため、アプリ全体を Provider でラップします。

typescript// src/pages/_app.tsx
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: '/api/trpc', // tRPC エンドポイントの URL
        }),
      ],
    })
  );

QueryClienttrpcClient を初期化し、後述の Provider に渡します。httpBatchLink を使うことで、複数の API 呼び出しを 1 つの HTTP リクエストにまとめて効率化できます。

typescript  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <Component {...pageProps} />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

export default MyApp;

tRPC の Provider と React Query の Provider でアプリをラップすることで、全てのコンポーネントで tRPC hooks が使えるようになります。

3. Todo コンポーネントの実装

tRPC hooks を使って Todo の一覧表示と操作を実装します。

typescript// src/pages/index.tsx
import { trpc } from '../utils/trpc';
import { useState } from 'react';

export default function Home() {
  const [title, setTitle] = useState('');

  // Todo 一覧を取得(型安全な useQuery)
  const { data: todos, refetch } = trpc.getTodos.useQuery();

  // Todo 追加(型安全な useMutation)
  const addMutation = trpc.addTodo.useMutation({
    onSuccess: () => refetch(), // 追加後に再取得
  });

trpc.getTodos.useQuery() で Todo 一覧を取得します。戻り値の todos は自動的に型推論され、エディタで補完が効きます。addMutation も同様に型安全です。

typescript// Todo 更新
const updateMutation = trpc.updateTodo.useMutation({
  onSuccess: () => refetch(),
});

// Todo 削除
const deleteMutation = trpc.deleteTodo.useMutation({
  onSuccess: () => refetch(),
});

const handleAdd = () => {
  if (title.trim()) {
    addMutation.mutate({ title }); // 型安全な入力
    setTitle('');
  }
};

updateMutationdeleteMutation も同様に定義します。mutate() メソッドに渡す引数も型チェックされるため、タイポや型ミスがあればビルド時にエラーになります。

typescript  return (
    <div>
      <h1>tRPC Todo App</h1>

      {/* Todo 追加フォーム */}
      <div>
        <input
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="新しいタスク"
        />
        <button onClick={handleAdd}>追加</button>
      </div>

      {/* Todo 一覧 */}
      <ul>
        {todos?.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() =>
                updateMutation.mutate({
                  id: todo.id,
                  completed: !todo.completed,
                })
              }
            />
            <span>{todo.title}</span>
            <button onClick={() => deleteMutation.mutate({ id: todo.id })}>
              削除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Todo の一覧を表示し、チェックボックスで完了状態を更新、削除ボタンで Todo を削除できます。全ての操作が型安全で、エディタの補完が効くため、開発体験が格段に向上します。

実装の要点まとめ

以下の表で、実装したコードの行数と役割を整理します。

#ファイル行数役割
1server​/​trpc.ts6 行tRPC の初期化
2server​/​router.ts約 40 行Todo CRUD の定義
3pages​/​api​/​trpc​/​[trpc].ts8 行Next.js API Route 設定
4utils​/​trpc.ts4 行tRPC Client 初期化
5pages​/​_app.tsx約 20 行Provider 設定
6pages​/​index.tsx約 50 行Todo UI コンポーネント

合計で約 130 行となりますが、実際の API ロジック部分(CRUD 定義)は 約 50 行 に収まっています。REST API や GraphQL と比べて、型定義やスキーマファイルが一切不要で、コード量が圧倒的に少ないことがわかりますね。

動作確認

開発サーバーを起動して、動作を確認しましょう。

bash# 開発サーバーを起動
yarn dev

ブラウザで http:​/​​/​localhost:3000 にアクセスすると、Todo アプリが表示されます。タスクを追加・更新・削除してみてください。全ての操作が型安全に動作し、エディタでは自動補完が効いていることを実感できるはずです。

開発者ツールのネットワークタブを見ると、tRPC が HTTP POST リクエストでサーバーと通信していることが確認できます。内部的には JSON-RPC のような形式でデータがやり取りされています。

まとめ

tRPC を使えば、わずか 50 行程度のコードで型安全な Todo API を実装でき、フルスタック TypeScript 開発の魅力を存分に体験できます。本記事で学んだ内容を振り返ってみましょう。

tRPC のメリット

#メリット詳細
1型の自動共有サーバーの型がクライアントに自動で伝わる
2コード量の削減スキーマや型定義ファイルが不要
3開発体験の向上エディタの自動補完でミスを防げる
4実行時エラーの削減ビルド時に型エラーを検出できる
5React Query 統合キャッシュやリフェッチが簡単に実装可能

tRPC が適しているケース

tRPC は以下のようなプロジェクトで特に威力を発揮します。

  • フルスタック TypeScript のプロジェクト(Next.js、Remix など)
  • モノレポ構成 でサーバーとクライアントを同じリポジトリで管理
  • 小〜中規模の API で、GraphQL のような複雑な仕組みが不要
  • スタートアップや MVP 開発 で、迅速にプロトタイプを作りたい場合
  • 型安全性を重視 するチーム開発

次のステップ

今回は基本的な CRUD 操作を実装しましたが、tRPC にはさらに高度な機能があります。

  1. 認証・認可 - Context を使ってユーザー情報を Procedure に渡す
  2. エラーハンドリング - TRPCError でカスタムエラーを定義
  3. Middleware - ログ出力や権限チェックを共通化
  4. Subscription - WebSocket を使ったリアルタイム通信
  5. DB 連携 - Prisma などの ORM と組み合わせて本格的な API を構築

tRPC の公式ドキュメントには、これらのトピックについて詳しく解説されていますので、ぜひ挑戦してみてください。型安全なフルスタック開発の世界が、さらに広がっていくことでしょう。

関連リンク