tRPCとは
🔷 tRPCとは
Section titled “🔷 tRPCとは”tRPCは、TypeScriptファーストのRPCフレームワークです。エンドツーエンドの型安全性を提供し、TypeScriptの型システムを活用してAPIを構築できます。
🎯 なぜtRPCが必要なのか
Section titled “🎯 なぜtRPCが必要なのか”❌ 従来のAPI開発の問題
Section titled “❌ 従来のAPI開発の問題”❌ 問題のある実装:
// フロントエンド: APIの型が不明確const response = await fetch('/api/users/1');const user = await response.json();// 問題: userの型が不明確、実行時エラーのリスク
// バックエンド: 型の定義が重複interface User { id: number; name: string; email: string;}
app.get('/api/users/:id', (req, res) => { const user: User = getUserById(req.params.id); res.json(user);});⚠️ 影響:
- ❌ 型の不一致
- ⚠️ 実行時エラー
- 📉 開発効率の低下
✅ tRPCによる解決
Section titled “✅ tRPCによる解決”✅ 改善された実装:
// バックエンド: 型安全なAPI定義import { initTRPC } from '@trpc/server';import { z } from 'zod';
const t = initTRPC.context().create();
export const appRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.number() })) .query(({ input }) => { return getUserById(input.id); }),
createUser: t.procedure .input(z.object({ name: z.string(), email: z.string().email(), })) .mutation(({ input }) => { return createUser(input); }),});
export type AppRouter = typeof appRouter;// フロントエンド: 型安全なAPI呼び出しimport { createTRPCProxyClient, httpBatchLink } from '@trpc/client';import type { AppRouter } from './server';
const trpc = createTRPCProxyClient<AppRouter>({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', }), ],});
// 型安全なAPI呼び出しconst user = await trpc.getUser.query({ id: 1 });// userの型が自動的に推論される✅ メリット:
- ✅ エンドツーエンドの
型安全性 - ✅ 型の自動推論
- ✅ 実行時エラーの削減
- 📈 開発効率の向上
🎯 tRPCの特徴
Section titled “🎯 tRPCの特徴”✅ 1. エンドツーエンドの型安全性
Section titled “✅ 1. エンドツーエンドの型安全性”📋 定義: バックエンドで定義した型が、フロントエンドでも自動的に利用できます。
// バックエンドで定義export const appRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.number() })) .query(({ input }) => { // input.idはnumber型として推論される return getUserById(input.id); }),});
// フロントエンドで使用const user = await trpc.getUser.query({ id: 1 });// userの型が自動的に推論される⚠️ 型安全性の誤解:
TypeScriptの型は実行時に消えます。
「型安全だから安心」は幻想です。実行時の防御は別途必要です。
✅ 実行時の防御(Zodでサニタイズ):
const createUserSchema = z .object({ name: z.string().min(1), email: z.string().email(), age: z.number().min(0).max(150), }) .strict() .transform((input) => ({ ...input, email: input.email.toLowerCase().trim(), }));- .strict(): 未知のプロパティを排除する
- .transform(): 入力を正規化してサニタイズする
フロントからのinputを盲信せず、バックエンドで必ず整形します。
✅ 2. Zodによるバリデーション
Section titled “✅ 2. Zodによるバリデーション”📋 定義:
Zodスキーマを使用して、入力のバリデーションと型定義を同時に行います。
import { z } from 'zod';
const createUserSchema = z.object({ name: z.string().min(1), email: z.string().email(), age: z.number().min(0).max(150),});
export const appRouter = t.router({ createUser: t.procedure .input(createUserSchema) .mutation(({ input }) => { // inputはcreateUserSchemaの型として推論される // バリデーションとサニタイズが自動的に実行される return createUser(input); }),});⚠️ 2.5 密結合の罠(DTOを挟む)
Section titled “⚠️ 2.5 密結合の罠(DTOを挟む)”バックエンドのDBエンティティをそのまま返すのは悪手です。
内部構造が変わるたびにフロントが壊れます。
✅ DTOを挟む:
type UserDto = { id: number; name: string; email: string;};
const toUserDto = (user: UserEntity): UserDto => ({ id: user.id, name: user.name, email: user.email,});
getUser: t.procedure .input(z.object({ id: z.number() })) .query(async ({ input }) => { const user = await getUserById(input.id); return toUserDto(user); }),export type AppRouterは便利ですが、内部構造の露出を正当化しません。
📋 3. プロシージャの種類
Section titled “📋 3. プロシージャの種類”📖 Query(読み取り):
getUser: t.procedure .input(z.object({ id: z.number() })) .query(({ input }) => { return getUserById(input.id); }),✏️ Mutation(書き込み):
createUser: t.procedure .input(z.object({ name: z.string(), email: z.string() })) .mutation(({ input }) => { return createUser(input); }),⚠️ 3.5 パフォーマンスの盲点(バッチ処理の副作用)
Section titled “⚠️ 3.5 パフォーマンスの盲点(バッチ処理の副作用)”httpBatchLinkは魔法ではありません。
1つの重いクエリが全レスポンスを遅延させます。
対策:
- 重い処理はバッチ対象から除外する
httpBatchLinkと通常リンクを分離する- 優先度の高いクエリは単独リンクで処理する
tRPCの実装例
Section titled “tRPCの実装例”Next.jsでの実装
Section titled “Next.jsでの実装”バックエンド(API Route):
import { createNextApiHandler } from '@trpc/server/adapters/next';import { appRouter } from '../../../server/routers/_app';
export default createNextApiHandler({ router: appRouter, createContext: () => ({}),});フロントエンド:
import { createTRPCNext } from '@trpc/next';import type { AppRouter } from '../server/routers/_app';
export const trpc = createTRPCNext<AppRouter>({ config() { return { url: '/api/trpc', }; },});
// コンポーネントでの使用function UserProfile({ userId }: { userId: number }) { const { data: user, isLoading } = trpc.getUser.useQuery({ id: userId });
if (isLoading) { return <div>Loading...</div>; }
return <div>{user.name}</div>;}🔥 エラーハンドリングの解像度を上げる
Section titled “🔥 エラーハンドリングの解像度を上げる”TRPCErrorで構造化されたエラーを返すことで、クライアント側が機械的に判別できます。
import { TRPCError } from '@trpc/server';
getUser: t.procedure .input(z.object({ id: z.number() })) .query(async ({ input }) => { const user = await getUserById(input.id); if (!user) { throw new TRPCError({ code: 'NOT_FOUND', message: `User ${input.id} not found`, }); } return user; }),使うべきコード例:
BAD_REQUEST: 入力不正UNAUTHORIZED: 認証不足FORBIDDEN: 権限不足NOT_FOUND: 存在しない
Expressでの実装
Section titled “Expressでの実装”バックエンド:
import express from 'express';import { createExpressMiddleware } from '@trpc/server/adapters/express';import { appRouter } from './routers/_app';
const app = express();
app.use( '/trpc', createExpressMiddleware({ router: appRouter, createContext: () => ({}), }));
app.listen(3000);フロントエンド:
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';import type { AppRouter } from './server/routers/_app';
const trpc = createTRPCProxyClient<AppRouter>({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', }), ],});
// 使用const user = await trpc.getUser.query({ id: 1 });⚖️ tRPC vs GraphQL vs RESTful API
Section titled “⚖️ tRPC vs GraphQL vs RESTful API”| 項目 | tRPC | GraphQL | RESTful API |
|---|---|---|---|
型安全性 | エンドツーエンド | 限定的 | なし |
| 学習コスト | 中 | 高 | 低 |
パフォーマンス | 高い | 中程度 | 中程度 |
| フレームワーク | TypeScript必須 | 言語非依存 | 言語非依存 |
バリデーション | Zod統合 | スキーマ定義 | 手動 |
🎯 使い分け
Section titled “🎯 使い分け”✅ tRPCを使うべき場合:
- 🔷 TypeScriptプロジェクト
- ✅ エンドツーエンドの
型安全性が必要 - 🔄 フロントエンドとバックエンドが同じコードベース
- 📈 開発効率を重視
✅ GraphQLを使うべき場合:
- 📊 複雑なデータ取得が必要
- 🔄 クライアントが多様なデータを要求
- 🌍 言語非依存が必要
✅ RESTful APIを使うべき場合:
- 📝 シンプルな
API - 🌍 広くサポートされている形式が必要
- 🌍 言語非依存が必要
tRPCのポイント:
- ✅ エンドツーエンドの型安全性: バックエンドとフロントエンドで型を共有
- ✅ Zod統合:
バリデーションと型定義を同時に - 📈 開発効率: 型の自動推論により開発効率が向上
- 🔷 TypeScriptファースト: TypeScriptの型システムを最大限に活用
適切にtRPCを使用することで、型安全で効率的なAPI開発ができます。