Skip to content

tRPCとは

tRPCは、TypeScriptファーストのRPCフレームワークです。エンドツーエンドの型安全性を提供し、TypeScriptの型システムを活用して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);
});

⚠️ 影響:

  • ❌ 型の不一致
  • ⚠️ 実行時エラー
  • 📉 開発効率の低下

✅ 改善された実装:

// バックエンド: 型安全な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の型が自動的に推論される

✅ メリット:

  • ✅ エンドツーエンドの型安全性
  • ✅ 型の自動推論
  • ✅ 実行時エラーの削減
  • 📈 開発効率の向上

✅ 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を盲信せず、バックエンドで必ず整形します。

📋 定義: 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は便利ですが、内部構造の露出を正当化しません

📖 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と通常リンクを分離する
  • 優先度の高いクエリは単独リンクで処理する

バックエンド(API Route):

pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';
export default createNextApiHandler({
router: appRouter,
createContext: () => ({}),
});

フロントエンド:

utils/trpc.ts
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: 存在しない

バックエンド:

server.ts
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);

フロントエンド:

client.ts
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 });
項目tRPCGraphQLRESTful API
型安全性エンドツーエンド限定的なし
学習コスト
パフォーマンス高い中程度中程度
フレームワークTypeScript必須言語非依存言語非依存
バリデーションZod統合スキーマ定義手動

✅ tRPCを使うべき場合:

  • 🔷 TypeScriptプロジェクト
  • ✅ エンドツーエンドの型安全性が必要
  • 🔄 フロントエンドとバックエンドが同じコードベース
  • 📈 開発効率を重視

✅ GraphQLを使うべき場合:

  • 📊 複雑なデータ取得が必要
  • 🔄 クライアントが多様なデータを要求
  • 🌍 言語非依存が必要

✅ RESTful APIを使うべき場合:

  • 📝 シンプルなAPI
  • 🌍 広くサポートされている形式が必要
  • 🌍 言語非依存が必要

tRPCのポイント:

  • エンドツーエンドの型安全性: バックエンドとフロントエンドで型を共有
  • Zod統合: バリデーションと型定義を同時に
  • 📈 開発効率: 型の自動推論により開発効率が向上
  • 🔷 TypeScriptファースト: TypeScriptの型システムを最大限に活用

適切にtRPCを使用することで、型安全で効率的なAPI開発ができます。