T-CREATOR

tRPC の始め方:Next.js App Router と Zod を使った最小構成テンプレート

tRPC の始め方:Next.js App Router と Zod を使った最小構成テンプレート

tRPC を使えば、API の型定義を一度書くだけで、フロントエンドとバックエンドの両方で型安全な通信が実現できます。しかし、初めて導入する際には「どこから手をつければいいのか」「どんなパッケージが必要なのか」と迷ってしまうことも多いのではないでしょうか。

本記事では、Next.js の App Router と Zod を組み合わせた、tRPC の最小構成テンプレートを段階的に解説します。実際に手を動かしながら、tRPC の基本的なセットアップから、最初の API エンドポイント作成までを体験できる内容となっています。

背景

tRPC が解決する開発体験の課題

従来の Web アプリケーション開発では、フロントエンドとバックエンドが別々に型定義を管理する必要がありました。API の仕様が変更されるたびに、両側のコードを手動で同期させる作業が発生し、型の不一致によるバグも頻発していました。

tRPC は、TypeScript のエコシステムを最大限に活用することで、この課題を根本から解決します。サーバー側で定義した型が自動的にクライアント側にも反映されるため、型安全性を保ちながら開発スピードを大幅に向上できるのです。

Next.js App Router との相性

Next.js 13 以降で導入された App Router は、React Server Components をベースとした新しいアーキテクチャです。tRPC は App Router の設計思想とも相性が良く、以下のメリットがあります。

  • Server Actions との統合がスムーズ
  • Server Components からの直接呼び出しが可能
  • ファイルベースルーティングとの親和性が高い

以下の図は、tRPC を使った Next.js アプリケーションの基本的なデータフローを示しています。

mermaidflowchart LR
  client["クライアント<br/>(React Component)"] -->|tRPC Client で呼び出し| router["tRPC Router<br/>(API 定義)"]
  router -->|Zod でバリデーション| procedure["Procedure<br/>(処理ロジック)"]
  procedure -->|型安全なレスポンス| router
  router -->|型推論された結果| client

  style router fill:#e1f5ff
  style procedure fill:#fff4e1

このように、tRPC Router を中心として、クライアントからの呼び出しとサーバー側の処理が型安全に連携します。

Zod によるバリデーション強化

Zod は TypeScript 向けのスキーマバリデーションライブラリです。tRPC と組み合わせることで、以下の利点が得られます。

#項目説明
1ランタイムバリデーション実行時にデータの整合性をチェック
2型推論Zod スキーマから TypeScript の型を自動生成
3エラーハンドリングわかりやすいエラーメッセージの提供

課題

tRPC 導入時によくある悩み

tRPC を初めて導入する際、多くの開発者が以下のような課題に直面します。

1. パッケージの組み合わせがわからない

tRPC 本体だけでなく、Next.js アダプター、React Query の連携、Zod の設定など、複数のパッケージを適切に組み合わせる必要があります。公式ドキュメントを読んでも、最小構成が何なのかが初見ではわかりにくいという声をよく聞きますね。

2. App Router 特有の設定方法

Next.js の Pages Router と App Router では、tRPC の設定方法が異なります。特に App Router では、Server Components と Client Components の使い分けや、新しい API Routes の構造を理解する必要があるでしょう。

3. ディレクトリ構成の設計

プロジェクトが成長した時のことを考えて、最初から適切なディレクトリ構成を設計したいものです。しかし、tRPC のルーターやプロシージャをどこに配置すべきか、初めての場合は判断が難しいかもしれません。

以下の図は、tRPC 導入時の典型的な課題フローを示しています。

mermaidflowchart TD
  start["tRPC を導入したい"] --> q1{"パッケージの<br/>選定は?"}
  q1 -->|不明確| problem1["どのパッケージが<br/>必要か迷う"]
  q1 -->|明確| q2{"App Router の<br/>設定は?"}

  problem1 --> research1["公式ドキュメントを<br/>読み込む"]
  research1 --> q2

  q2 -->|不明確| problem2["設定ファイルの<br/>配置場所が不明"]
  q2 -->|明確| q3{"ディレクトリ<br/>構成は?"}

  problem2 --> research2["サンプルコードを<br/>探す"]
  research2 --> q3

  q3 -->|不明確| problem3["拡張性を考えた<br/>設計が難しい"]
  q3 -->|明確| success["導入完了"]

  problem3 --> solution["最小構成テンプレート<br/>の活用"]
  solution --> success

  style problem1 fill:#ffe1e1
  style problem2 fill:#ffe1e1
  style problem3 fill:#ffe1e1
  style success fill:#e1ffe1
  style solution fill:#fff4e1

これらの課題を解決するために、本記事では実践的な最小構成テンプレートを提供します。

解決策

最小構成テンプレートの全体像

本記事で構築する tRPC の最小構成は、以下の要素で構成されます。

#要素役割
1Next.js App Routerアプリケーションのベースフレームワーク
2tRPC ServerAPI のルーターと処理ロジック
3tRPC Clientクライアント側からの呼び出し
4Zod入力バリデーションとスキーマ定義
5TanStack Queryデータフェッチングとキャッシング

プロジェクトのディレクトリ構成

最小構成でありながら、将来の拡張性も考慮したディレクトリ構成は以下の通りです。

typescript// プロジェクトのディレクトリ構造
next-trpc-template/
├── src/
│   ├── app/                    // Next.js App Router
│   │   ├── api/
│   │   │   └── trpc/
│   │   │       └── [trpc]/
│   │   │           └── route.ts    // tRPC API ハンドラー
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── server/                 // サーバー側のコード
│   │   ├── routers/            // tRPC ルーター
│   │   │   ├── _app.ts         // ルートルーター
│   │   │   └── example.ts      // 個別ルーター
│   │   └── trpc.ts             // tRPC 初期化設定
│   └── trpc/                   // クライアント側のコード
│       ├── client.ts            // tRPC クライアント設定
│       └── Provider.tsx         // React Query プロバイダー
├── package.json
└── tsconfig.json

この構成により、サーバー側とクライアント側のコードが明確に分離され、チーム開発でも混乱が起きにくくなります。

セットアップの段階的アプローチ

tRPC のセットアップは、以下の 5 つのステップで進めます。

  1. プロジェクトの初期化と依存パッケージのインストール
  2. サーバー側の tRPC 設定
  3. API ルーターの作成
  4. クライアント側の tRPC 設定
  5. 実際の呼び出しテスト

それぞれのステップを詳しく見ていきましょう。

具体例

ステップ 1: プロジェクトの初期化

まず、Next.js プロジェクトを作成します。TypeScript と App Router を有効にした状態で初期化しましょう。

bash# Next.js プロジェクトの作成
yarn create next-app next-trpc-template --typescript --app --tailwind
cd next-trpc-template

プロジェクトが作成されたら、必要なパッケージをインストールします。

bash# tRPC 関連のパッケージをインストール
yarn add @trpc/server @trpc/client @trpc/react-query @trpc/next
bash# TanStack Query のインストール(React Query の後継)
yarn add @tanstack/react-query
bash# Zod のインストール(バリデーション用)
yarn add zod

これで、tRPC の開発に必要なすべてのパッケージが揃いました。

ステップ 2: サーバー側の tRPC 初期化設定

tRPC を使うための基本設定を作成します。src​/​server​/​trpc.ts ファイルを新規作成してください。

typescript// src/server/trpc.ts
// tRPC の初期化とコンテキスト定義

import { initTRPC } from '@trpc/server';
import { cache } from 'react';

次に、コンテキストの作成関数を定義します。コンテキストは、すべてのプロシージャで共有されるデータ(認証情報など)を保持します。

typescript// src/server/trpc.ts
// コンテキストの作成(最小構成では空のオブジェクト)

export const createContext = cache(async () => {
  return {};
});

// コンテキストの型を推論
export type Context = Awaited<
  ReturnType<typeof createContext>
>;

tRPC インスタンスを初期化し、プロシージャ作成用のヘルパーをエクスポートします。

typescript// src/server/trpc.ts
// tRPC インスタンスの初期化

const t = initTRPC.context<Context>().create();

// ルーター作成用のヘルパー
export const router = t.router;

// パブリックプロシージャ(認証不要の処理)
export const publicProcedure = t.procedure;

これで、tRPC の基本設定が完了しました。routerpublicProcedure を使って、API エンドポイントを定義していきます。

ステップ 3: API ルーターの作成

実際に呼び出せる API エンドポイントを作成しましょう。まず、サンプルとなる example ルーターを作成します。

typescript// src/server/routers/example.ts
// サンプル API ルーター

import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

シンプルな挨拶を返す API を定義します。入力値を Zod でバリデーションし、型安全なレスポンスを返しましょう。

typescript// src/server/routers/example.ts
// 挨拶を返すプロシージャ

export const exampleRouter = router({
  // 挨拶を返す API
  hello: publicProcedure
    // 入力スキーマの定義(Zod を使用)
    .input(
      z.object({
        name: z.string().min(1, '名前は必須です'),
      })
    )
    // 処理ロジック
    .query(({ input }) => {
      return {
        message: `こんにちは、${input.name}さん!`,
        timestamp: new Date().toISOString(),
      };
    }),
});

次に、複数のルーターをまとめるルートルーターを作成します。

typescript// src/server/routers/_app.ts
// すべてのルーターを統合するルートルーター

import { router } from '../trpc';
import { exampleRouter } from './example';

exampleRouter をルートルーターに統合します。今後、新しいルーターを追加する際もここに追加していきます。

typescript// src/server/routers/_app.ts
// ルーターの統合

export const appRouter = router({
  example: exampleRouter,
});

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

これで、example.hello というエンドポイントが作成されました。クライアントからは trpc.example.hello.useQuery() のように呼び出せます。

ステップ 4: Next.js API Route の設定

tRPC を Next.js の API Route として公開するための設定を行います。src​/​app​/​api​/​trpc​/​[trpc]​/​route.ts ファイルを作成してください。

typescript// src/app/api/trpc/[trpc]/route.ts
// tRPC ハンドラーの設定

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';
import { createContext } from '@/server/trpc';

HTTP リクエストを tRPC ハンドラーに渡す関数を定義します。GET と POST の両方に対応させましょう。

typescript// src/app/api/trpc/[trpc]/route.ts
// リクエストハンドラーの実装

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: createContext,
  });

// GET と POST リクエストをエクスポート
export { handler as GET, handler as POST };

これで、​/​api​/​trpc​/​* へのリクエストが tRPC によって処理されるようになりました。

以下の図は、リクエストがどのように処理されるかを示しています。

mermaidsequenceDiagram
  participant Client as クライアント
  participant Route as API Route<br/>/api/trpc/[trpc]
  participant Handler as fetchRequestHandler
  participant Router as appRouter
  participant Procedure as example.hello

  Client->>Route: POST /api/trpc/example.hello
  Route->>Handler: リクエストを転送
  Handler->>Handler: createContext() 実行
  Handler->>Router: ルーティング処理
  Router->>Procedure: input バリデーション<br/>(Zod)
  Procedure->>Procedure: 処理ロジック実行
  Procedure->>Router: レスポンス返却
  Router->>Handler: 型安全なレスポンス
  Handler->>Route: JSON レスポンス
  Route->>Client: 型推論された結果

  Note over Client,Procedure: すべての通信が型安全

ステップ 5: クライアント側の tRPC 設定

クライアント側から tRPC を呼び出すための設定を行います。まず、tRPC クライアントを作成しましょう。

typescript// src/trpc/client.ts
// tRPC クライアントの設定

import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';

型安全な tRPC クライアントを生成します。AppRouter の型を使うことで、自動補完とエラーチェックが有効になります。

typescript// src/trpc/client.ts
// tRPC クライアントの生成

export const trpc = createTRPCReact<AppRouter>();

次に、React Query のプロバイダーを設定します。この設定により、クライアントコンポーネントから tRPC を使えるようになります。

typescript// src/trpc/Provider.tsx
// tRPC プロバイダーコンポーネント

'use client';

import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from './client';

プロバイダーコンポーネントを実装します。httpBatchLink を使うことで、複数のリクエストを一度にまとめて送信できます。

typescript// src/trpc/Provider.tsx
// プロバイダーの実装

export function TRPCProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  // QueryClient の初期化(コンポーネントごとに一度だけ)
  const [queryClient] = useState(() => new QueryClient());

  // tRPC クライアントの初期化
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          // API エンドポイントの URL
          url: '/api/trpc',
        }),
      ],
    })
  );

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

ルートレイアウトにプロバイダーを追加します。

typescript// src/app/layout.tsx
// プロバイダーの追加

import { TRPCProvider } from '@/trpc/Provider';
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'tRPC + Next.js テンプレート',
  description: 'tRPC と Next.js App Router の最小構成',
};
typescript// src/app/layout.tsx
// レイアウトコンポーネント

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='ja'>
      <body>
        <TRPCProvider>{children}</TRPCProvider>
      </body>
    </html>
  );
}

これで、すべてのページコンポーネントから tRPC を使えるようになりました。

ステップ 6: 実際の呼び出しテスト

それでは、実際に tRPC API を呼び出してみましょう。ホームページを編集して、挨拶を表示するコンポーネントを作成します。

typescript// src/app/page.tsx
// ホームページコンポーネント

'use client';

import { trpc } from '@/trpc/client';
import { useState } from 'react';

入力フォームと結果表示を含むコンポーネントを実装します。

typescript// src/app/page.tsx
// メインコンポーネント

export default function Home() {
  const [name, setName] = useState('太郎');

  // tRPC クエリの呼び出し(型安全)
  const { data, isLoading, error } = trpc.example.hello.useQuery({
    name: name,
  });

  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24">
      <div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm">
        <h1 className="text-4xl font-bold mb-8">
          tRPC テンプレート
        </h1>

        {/* 入力フォーム */}
        <div className="mb-4">
          <label className="block mb-2">
            名前を入力してください:
          </label>
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className="border p-2 rounded"
          />
        </div>

結果の表示とエラーハンドリングを実装します。

typescript// src/app/page.tsx
// 結果の表示

        {/* ローディング状態 */}
        {isLoading && <p>読み込み中...</p>}

        {/* エラー表示 */}
        {error && (
          <div className="text-red-500">
            エラー: {error.message}
          </div>
        )}

        {/* 成功時の表示 */}
        {data && (
          <div className="bg-blue-100 p-4 rounded">
            <p className="text-lg">{data.message}</p>
            <p className="text-sm text-gray-600 mt-2">
              実行時刻: {new Date(data.timestamp).toLocaleString('ja-JP')}
            </p>
          </div>
        )}
      </div>
    </main>
  );
}

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

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

ブラウザで http:​/​​/​localhost:3000 にアクセスすると、入力した名前に応じた挨拶メッセージが表示されます。入力フォームに値を入力すると、リアルタイムで API が呼び出され、結果が更新されることが確認できるでしょう。

TypeScript の型推論の確認

VSCode などのエディタで、以下の点を確認してみてください。

#確認ポイント期待される動作
1trpc.example.hello の自動補完ルーターとプロシージャが候補に表示される
2useQuery の引数{ name: string } が必須と認識される
3data の型messagetimestamp プロパティが推論される
4存在しないプロシージャ呼び出しTypeScript エラーが表示される

これらがすべて正しく動作していれば、tRPC の型安全性が機能している証拠です。

エラーハンドリングの追加

Zod によるバリデーションエラーを確認するために、わざと空文字を送信してみましょう。

typescript// src/app/page.tsx
// バリデーションエラーのテスト用コード(参考)

// 入力フォームで空文字を入力すると、以下のエラーが表示されます
// Error: [
//   {
//     "code": "too_small",
//     "minimum": 1,
//     "type": "string",
//     "inclusive": true,
//     "exact": false,
//     "message": "名前は必須です",
//     "path": ["name"]
//   }
// ]

このように、Zod で定義したエラーメッセージがクライアント側に正しく伝わります。エラーメッセージは日本語で定義できるため、ユーザーフレンドリーなバリデーションが実現できますね。

図で理解できる要点

  • tRPC の設定は、サーバー初期化 → ルーター作成 → API Route 設定 → クライアント設定の順に進める
  • すべての通信が型安全で、サーバー側の変更が即座にクライアント側のエラーとして検出される
  • Zod によるバリデーションは、型推論とランタイムチェックの両方を提供する

まとめ

本記事では、tRPC を Next.js App Router と Zod を使って最小構成でセットアップする方法を段階的に解説しました。

構築した環境では、以下の機能が実現されています。

  • 完全な型安全性: サーバーからクライアントまで、型の整合性が自動的に保証されます
  • Zod によるバリデーション: 実行時のデータ検証とわかりやすいエラーメッセージを提供します
  • App Router 対応: Next.js の最新アーキテクチャに対応した設定となっています
  • 拡張性: 新しいルーターやプロシージャを追加しやすい構成になっています

この最小構成をベースに、認証機能やデータベース連携、ミドルウェアなどを追加していくことで、本格的なアプリケーション開発が可能になります。tRPC の型安全性を活かしながら、開発スピードと品質の両立を実現してください。

初めての tRPC 導入でも、この記事の手順に従えば、迷うことなくセットアップできるはずです。ぜひ、実際に手を動かして、tRPC の開発体験を味わってみてくださいね。

関連リンク