tRPC 101の実装
tRPC(TypeScript Remote Procedure Call)はフロントエンドとバックエンドのギャップを排除し、クライアントからサーバー関数を直接呼び出せるようにします — 完全なTypeScript推論、コード生成なし、保守するAPIスキーマなしで。
tRPCとは?
tRPCはTypeScriptでエンドツーエンドの型安全APIを構築するためのフレームワークです。RESTルートやGraphQLスキーマを定義する代わりに、サーバー上にプレーンなTypeScript関数を書き、クライアントからローカル関数のように呼び出します。バックエンドの戻り値の型を変更すると、フロントエンドはすぐに型エラーを表示します — ビルドステップなし、コード生成なし。
RESTやGraphQLよりもtRPCをなぜ選ぶのか?
| REST | GraphQL | tRPC | |
| 型安全性 | 手動 | コード生成で対応 | 組み込み |
| ボイラープレート | 高い | 高い | 最小限 |
| スキーマ定義 | OpenAPI / Swagger | SDL + Resolvers | 不要 |
| 学習曲線 | 低い | 急峻 | 低い |
| 最適な用途 | 公開API | 複雑なグラフデータ | フルスタックTSモノレポ |
tRPCはフロントエンドとバックエンドの両方がTypeScriptの場合に最適です。非TSクライアントに消費される公開APIが必要な場合は、RESTまたはGraphQLがより適しています。
フロントエンドとバックエンドの橋渡し
tRPCはUIとサーバーロジックの間に位置する型安全APIレイヤーとして機能します。各ピースがどのように接続するかは以下の通りです:
┌─────────────────────────────────────────────────┐
│ Frontend (React / Next.js) │
│ │
│ trpc.dashboard.getStats.useQuery() │
│ ↕ full type inference, no fetch() │
├─────────────────────────────────────────────────┤
│ tRPC Client → httpBatchLink → /api/trpc │
├─────────────────────────────────────────────────┤
│ tRPC Router (API Layer) │
│ │
│ ├── dashboardRouter │
│ │ ├── getStats (query) │
│ │ └── updateConfig (mutation) │
│ ├── userRouter │
│ │ ├── me (query) │
│ │ └── updateProfile(mutation) │
│ └── notificationRouter │
│ └── onNew (subscription) │
├─────────────────────────────────────────────────┤
│ Handlers / Business Logic │
│ (DB queries, external APIs, transforms) │
└─────────────────────────────────────────────────┘
重要な洞察: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はオプションですが推奨されています — これにより、Date、Map、Setおよび他の非JSON型をクライアントとサーバー間でシームレスに送信できます。
2. tRPCを初期化する(サーバー)
/trpc/init.tsを作成します:
import { initTRPC, TRPCError } from '@trpc/server';
import { cache } from 'react';
import superjson from 'superjson';
// Context is created once per request
export const createTRPCContext = cache(async () => {
// Add auth session, DB connection, etc.
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({
// Query — fetches data
getStats: publicProcedure.query(async () => {
// Your DB call or business logic here
return {
totalUsers: 1250,
activeToday: 342,
revenue: 48000,
};
}),
// Mutation — modifies data
updateConfig: publicProcedure
.input(
z.object({
theme: z.enum(['light', 'dark']),
timezone: z.string(),
})
)
.mutation(async ({ input }) => {
// Save config to 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,
});
// Only export the TYPE — not the router itself
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';
// Create typed hooks
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();
// Type-safe query — return type is inferred automatically
const { data, isPending } = useQuery(trpc.dashboard.getStats.queryOptions());
// Type-safe mutation
const updateConfig = useMutation(
trpc.dashboard.updateConfig.mutationOptions()
);
if (isPending) return <div>Loading...</div>;
return (
<div>
<h2>Total Users: {data?.totalUsers}</h2>
<button onClick={() => updateConfig.mutate({ theme: 'dark', timezone: 'Asia/Tokyo' })}>
Switch to Dark
</button>
</div>
);
}
ミドルウェアと認証
ミドルウェアはルートを保護してコンテキストを充実させる方法です。一般的な認証パターンは以下の通りです:
import { TRPCError } from '@trpc/server';
import { middleware, publicProcedure } from './init';
// Auth middleware — checks for valid session
const isAuthed = middleware(async ({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You must be logged in',
});
}
// Swap context — downstream procedures now have a guaranteed userId
return next({
ctx: { userId: ctx.userId },
});
});
// Logging middleware
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;
});
// Create reusable procedure types
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 is guaranteed to be a string here
return await db.user.findUnique({ where: { id: ctx.userId } });
}),
});
エラーハンドリング
tRPCはHTTPステータスコードにマップする構造化されたエラーコードを提供します:
import { TRPCError } from '@trpc/server';
// In any procedure
throw new TRPCError({
code: 'NOT_FOUND', // → 404
message: 'User not found',
});
throw new TRPCError({
code: 'BAD_REQUEST', // → 400
message: 'Invalid email format',
});
throw new TRPCError({
code: 'FORBIDDEN', // → 403
message: 'Admin access required',
});
初期化時にエラーフォーマットをグローバルにカスタマイズして、Zodバリデーションの詳細などのメタデータを含めることもできます。
プロジェクト構造
/trpc
├── init.ts # tRPC initialization, context, base procedures
├── router.ts # Root appRouter merging all sub-routers
├── client.tsx # Client provider + hooks
└── routers/
├── dashboard.ts # Dashboard-related procedures
├── user.ts # User CRUD procedures
└── notification.ts
/app
└── api/trpc/[trpc]
└── route.ts # API handler (fetch adapter)
ルーターは小さく焦点を絞ったままにしてください。各ルーターはドメイン領域にマップされます — これは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 Query | tanstack.com/query |
| Zod 検証 | zod.dev |
| マイグレーションガイド(v10 → v11) | trpc.io/docs/migrate-from-v10-to-v11 |
最終更新:2026年3月28日