メインコンテンツまでスキップ
最新1mo ago

TRPC 101の実装

tRPC(TypeScript Remote Procedure Call)はフロントエンドとバックエンドのギャップを排除します。サーバー関数をクライアントから直接呼び出すことができます。完全なTypeScript推論、コード生成なし、管理するAPIスキーマなしです。


tRPCとは?

tRPCはTypeScriptで型安全なエンドツーエンドAPIを構築するためのフレームワークです。RESTルートやGraphQLスキーマを定義する代わりに、サーバーに通常のTypeScript関数を記述し、クライアントからローカル関数のように呼び出します。バックエンドの戻り値の型を変更すると、フロントエンドは即座に型エラーを表示します。ビルドステップやコード生成は不要です。

RESTやGraphQLと比べてなぜtRPC?

RESTGraphQLtRPC
型安全性手動コード生成で対応組み込み
ボイラープレート多い多い最小限
スキーマ定義OpenAPI / SwaggerSDL + リゾルバー不要
学習曲線低い急である低い
最適な用途パブリックAPI複雑なグラフデータフルスタックTSモノレポ

tRPCはフロントエンドとバックエンドの両方がTypeScriptの場合に最適です。非TSクライアントによって使用されるパブリックAPIが必要な場合は、RESTまたはGraphQLが適しています。


フロントエンド&バックエンド間のブリッジ

tRPCはUIとサーバーロジックの間に位置する型安全なAPIレイヤーとして機能します。ピースの接続方法は次の通りです:

┌─────────────────────────────────────────────────┐
│ フロントエンド (React / Next.js) │
│ │
│ trpc.dashboard.getStats.useQuery() │
│ ↕ 完全な型推論、fetch()なし │
├─────────────────────────────────────────────────┤
│ tRPCクライアント → httpBatchLink → /api/trpc │
├─────────────────────────────────────────────────┤
│ tRPCルーター(APIレイヤー) │
│ │
│ ├── dashboardRouter │
│ │ ├── getStats (query) │
│ │ └── updateConfig (mutation) │
│ ├── userRouter │
│ │ ├── me (query) │
│ │ └── updateProfile(mutation) │
│ └── notificationRouter │
│ └── onNew (subscription) │
├─────────────────────────────────────────────────┤
│ ハンドラー / ビジネスロジック │
│ (DBクエリ、外部API、変換) │
└─────────────────────────────────────────────────┘

重要なポイント:AppRouter型はサーバーからエクスポートされ、クライアントにインポートされます。型だけが境界を越えます。サーバーからクライアントへランタイムコードは流出しません。


コアコンセプト

ルーターとプロシージャー

ルーターは関連するプロシージャーをグループ化します。プロシージャーは単一のエンドポイントです。query(読み取り)、mutation(書き込み)、またはsubscription(リアルタイムストリーム)のいずれかです。

コンテキスト

コンテキストオブジェクトはリクエストごとに作成され、すべてのプロシージャーに渡されます。ここでデータベース接続、認証セッション、リクエストメタデータを添付します。

ミドルウェア

ミドルウェアはプロシージャーをラップして、認証、ロギング、レート制限などのクロスカッティングの関心事を処理します。ミドルウェアはコンテキストも交換できるため、下流のプロシージャーは充実したデータ(検証済みのuserオブジェクトなど)を取得します。

入力検証

tRPCはZod(または.parse()メソッドを持つ任意のバリデーター)を使用して、ランタイムで入力を検証します。検証された型は自動的にクライアントに流れます。


セットアップガイド(Next.js App Router + tRPC v11)

1. 依存関係をインストール

npm install @trpc/server @trpc/client @trpc/tanstack-react-query \
@tanstack/react-query@latest zod client-only server-only superjson

superjsonはオプションですが推奨されます。これにより、DateMapSet、およびその他の非JSONタイプをクライアントとサーバー間でシームレスに送信できます。

2. tRPCを初期化(サーバー)

/trpc/init.tsを作成:

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

// コンテキストはリクエストごとに一度作成されます
export const createTRPCContext = cache(async () => {
// 認証セッション、DB接続など
return {
userId: null as string | null,
};
});

const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;

3. 最初のルーターを作成

/trpc/routers/dashboard.tsを作成:

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

export const dashboardRouter = router({
// クエリ — データを取得
getStats: publicProcedure.query(async () => {
// ここにDBコールまたはビジネスロジック
return {
totalUsers: 1250,
activeToday: 342,
revenue: 48000,
};
}),

// ミューテーション — データを変更
updateConfig: publicProcedure
.input(
z.object({
theme: z.enum(['light', 'dark']),
timezone: z.string(),
})
)
.mutation(async ({ input }) => {
// 設定をDBに保存
return { success: true, applied: input };
}),
});

4. アプリルーターにマージ

/trpc/router.tsを作成:

import { router } from './init';
import { dashboardRouter } from './routers/dashboard';
import { userRouter } from './routers/user';

export const appRouter = router({
dashboard: dashboardRouter,
user: userRouter,
});

// 型だけをエクスポート — ルーター自体ではありません
export type AppRouter = typeof appRouter;

5. APIハンドラーを作成

/app/api/trpc/[trpc]/route.tsを作成:

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createTRPCContext } from '@/trpc/init';
import { appRouter } from '@/trpc/router';

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

export { handler as GET, handler as POST };

6. クライアントプロバイダーをセットアップ

/trpc/client.tsxを作成:

'use client';

import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
import superjson from 'superjson';
import type { AppRouter } from './router';

// 型付きフックを作成
const { TRPCProvider, useTRPC } = createTRPCContext\\<AppRouter>();

function getBaseUrl() {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}

export function TRPCClientProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
createTRPCClient\\<AppRouter>({
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
transformer: superjson,
}),
],
})
);

return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{children}
</TRPCProvider>
</QueryClientProvider>
);
}

export { useTRPC };

\\<TRPCClientProvider>でルートレイアウトをラップします。

7. フロントエンドからプロシージャーを呼び出す

'use client';

import { useTRPC } from '@/trpc/client';
import { useQuery, useMutation } from '@tanstack/react-query';

export function DashboardStats() {
const trpc = useTRPC();

// 型安全なクエリ — 戻り値の型は自動的に推論されます
const { data, isPending } = useQuery(trpc.dashboard.getStats.queryOptions());

// 型安全なミューテーション
const updateConfig = useMutation(
trpc.dashboard.updateConfig.mutationOptions()
);

if (isPending) return <div>読み込み中...</div>;

return (
<div>
<h2>総ユーザー数: {data?.totalUsers}</h2>
<button onClick={() => updateConfig.mutate({ theme: 'dark', timezone: 'Asia/Tokyo' })}>
ダークモードに切り替え
</button>
</div>
);
}


ミドルウェアと認証

ミドルウェアはルートを保護し、コンテキストを充実させる方法です。一般的な認証パターンは次の通りです:

import { TRPCError } from '@trpc/server';
import { middleware, publicProcedure } from './init';

// 認証ミドルウェア — 有効なセッションをチェック
const isAuthed = middleware(async ({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'ログインする必要があります',
});
}
// コンテキストを交換 — 下流のプロシージャーはuserIdが保証されます
return next({
ctx: { userId: ctx.userId },
});
});

// ロギングミドルウェア
const withLogging = middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
console.log(`${type} ${path}${Date.now() - start}ms`);
return result;
});

// 再利用可能なプロシージャータイプを作成
export const protectedProcedure = publicProcedure.use(isAuthed);
export const loggedProcedure = publicProcedure.use(withLogging);

認証が必要なルートにはpublicProcedureの代わりにprotectedProcedureを使用します:

export const userRouter = router({
me: protectedProcedure.query(async ({ ctx }) => {
// ctx.userIdはここで文字列であることが保証されます
return await db.user.findUnique({ where: { id: ctx.userId } });
}),
});


エラーハンドリング

tRPCはHTTPステータスコードにマップされる構造化エラーコードを提供します:

import { TRPCError } from '@trpc/server';

// 任意のプロシージャー内
throw new TRPCError({
code: 'NOT_FOUND', // → 404
message: 'ユーザーが見つかりません',
});

throw new TRPCError({
code: 'BAD_REQUEST', // → 400
message: 'メールアドレスの形式が無効です',
});

throw new TRPCError({
code: 'FORBIDDEN', // → 403
message: '管理者アクセスが必要です',
});

初期化時にグローバルにエラーフォーマットをカスタマイズして、Zod検証の詳細などのメタデータを含めることもできます。


プロジェクト構造

/trpc
├── init.ts # tRPC初期化、コンテキスト、基本プロシージャー
├── router.ts # すべてのサブルーターをマージするrootアプリルーター
├── client.tsx # クライアントプロバイダー + フック
└── routers/
├── dashboard.ts # ダッシュボード関連プロシージャー
├── user.ts # ユーザーCRUDプロシージャー
└── notification.ts
/app
└── api/trpc/[trpc]
└── route.ts # APIハンドラー(fetchアダプター)

ルーターは小さく焦点を絞ったものにしてください。各ルーターはドメインエリアにマップされます。APIが成長するにつれて、整理された状態を保ちます。


tRPCをいつ使うか

次の場合にtRPCを使用します:

  • フロントエンドとバックエンドの両方がTypeScript

  • モノレポまたはフルスタックフレームワーク(Next.js、SvelteKit)

  • コード生成オーバーヘッドなしの型安全性

  • 内部ダッシュボード、管理パネル、SaaSアプリを構築中

次の場合はtRPCをスキップします:

  • 非TypeScriptクライアントによって使用されるパブリックAPIが必要

  • バックエンドがGo、Python、または別の非TS言語

  • Server Actionsで十分なシンプルなCRUD

  • フルスタックTSの仮定が崩れるマイクロサービス


便利なリンク

リソースURL
tRPCドキュメントtrpc.io/docs
tRPC v11アナウンスメントtrpc.io/blog/announcing-trpc-v11
Next.js App Routerセットアップtrpc.io/docs/client/nextjs
TanStack React Querytanstack.com/query
Zodバリデーションzod.dev
マイグレーションガイド(v10 → v11)trpc.io/docs/migrate-from-v10-to-v11

最終更新: 2026年3月28日