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

tRPC 101の実装

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


tRPCとは?

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

RESTやGraphQLよりもtRPCをなぜ選ぶのか?

RESTGraphQLtRPC
型安全性手動コード生成で対応組み込み
ボイラープレート高い高い最小限
スキーマ定義OpenAPI / SwaggerSDL + 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はオプションですが推奨されています — これにより、DateMapSetおよび他の非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 Querytanstack.com/query
Zod 検証zod.dev
マイグレーションガイド(v10 → v11)trpc.io/docs/migrate-from-v10-to-v11

最終更新:2026年3月28日