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 の最小構成は、以下の要素で構成されます。
| # | 要素 | 役割 |
|---|---|---|
| 1 | Next.js App Router | アプリケーションのベースフレームワーク |
| 2 | tRPC Server | API のルーターと処理ロジック |
| 3 | tRPC Client | クライアント側からの呼び出し |
| 4 | Zod | 入力バリデーションとスキーマ定義 |
| 5 | TanStack 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 つのステップで進めます。
- プロジェクトの初期化と依存パッケージのインストール
- サーバー側の tRPC 設定
- API ルーターの作成
- クライアント側の tRPC 設定
- 実際の呼び出しテスト
それぞれのステップを詳しく見ていきましょう。
具体例
ステップ 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 の基本設定が完了しました。router と publicProcedure を使って、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 などのエディタで、以下の点を確認してみてください。
| # | 確認ポイント | 期待される動作 |
|---|---|---|
| 1 | trpc.example.hello の自動補完 | ルーターとプロシージャが候補に表示される |
| 2 | useQuery の引数 | { name: string } が必須と認識される |
| 3 | data の型 | message と timestamp プロパティが推論される |
| 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 の開発体験を味わってみてくださいね。
関連リンク
articletRPC 使い方入門:Todo API を 50 行で作るフルスタック体験
articletRPC アーキテクチャ設計:BFF とドメイン分割で肥大化を防ぐルータ戦略
articletRPC チートシート:Router/Procedure/ctx/useQuery/useMutation 早見表
articletRPC の始め方:Next.js App Router と Zod を使った最小構成テンプレート
articletRPC とは?型安全なフルスタック通信を実現する仕組みとメリット【2025 年版】
articleZod 合成パターン早見表:`object/array/tuple/record/map/set/intersection` 実例集
articleバックアップ戦略の決定版:WordPress の世代管理/災害復旧の型
articleYarn 運用ベストプラクティス:lockfile 厳格化・frozen-lockfile・Bot 更新方針
articleWebSocket のペイロード比較:JSON・MessagePack・Protobuf の速度とコスト検証
articleWeb Components イベント設計チート:`CustomEvent`/`composed`/`bubbles` 実例集
articleWebRTC SDP 用語チートシート:m=・a=・bundle・rtcp-mux を 10 分で総復習
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来