フロントエンドとバックエンドの間でAPI仕様が食い違い、実行時に初めてバグに気づく――TypeScriptプロジェクトでありがちなこの問題に悩んでいませんか?
本記事では、コード生成なしで型安全なAPIを構築できるライブラリ「tRPC」の使い方を、基礎から実践まで解説します。Next.jsとの連携方法や具体的な実装例も含め、すぐに使えるノウハウをお届けします。
tRPCとは?型安全なAPI開発を実現する仕組み
Webシステム開発で「フロントエンドとバックエンドで型が合わない」「API仕様変更に気づかずバグが発生した」という経験はありませんか?
tRPC(TypeScript Remote Procedure Call)は、TypeScriptの型システムを活用し、クライアントとサーバー間で完全な型安全性を実現するライブラリです。コード生成やスキーマ定義ファイルを必要とせず、TypeScriptのコードだけで型安全なAPIを構築できます。
従来のAPI開発における課題
現代のWeb開発では、フロントエンドとバックエンドを分離して開発するのが一般的ですが、以下のような課題があります。
- 型の不一致によるバグ:バックエンドでAPIの仕様を変更しても、フロントエンド側に気づかれず実行時エラーが発生(REST API設計のベストプラクティスも参照)
- 手動メンテナンスの負担:API仕様書を別途作成・更新する手間がかかり、ドキュメントとコードが乖離しやすい
- 開発効率の低下:APIの型情報を手動で定義する必要があり、コード補完が効かない
tRPCはTypeScriptの型推論を活用することで、これらの課題を根本から解決します。データベース操作にも型安全性を求めるならPrisma ORMとの組み合わせが効果的です。サーバー側で定義した型が自動的にクライアント側に反映されるため、型の不一致によるバグを開発段階で検出できます。
REST APIやGraphQLとの違い
REST APIとの比較
REST APIでは、型情報を別途手動で定義する必要があります。
// REST API:手動で型を定義
interface User {
id: number;
name: string;
}
const response = await fetch('/api/users/1');
const user: User = await response.json(); // 型の整合性は保証されない
tRPCでは型定義が自動的に推論されます。
// tRPC:型が自動的に推論される
const user = await trpc.user.getById.query({ id: 1 }); // 型安全
GraphQLとの比較
GraphQLはスキーマ定義言語(SDL)で別途スキーマを定義する必要があり、設定が複雑です。tRPCはTypeScriptのコードのみで完結し、シンプルで導入しやすい点が特徴です。
tRPCを選ぶメリット
開発効率の向上
型情報が自動的に共有されるため、以下の作業が不要になります。
- API仕様書の作成・更新
- クライアント側での型定義ファイルの手動作成
- APIの変更時の影響範囲の手動調査
バグの早期発見
型の不一致は実行時ではなく、コンパイル時(開発段階)に検出されます。これにより本番環境でのエラーを大幅に減らせます。
優れた開発体験
エディタの補完機能がフルに活用でき、APIの引数や戻り値の型がリアルタイムで表示されます。タイプミスや間違った引数を即座に検出できます。
学習コストの低さ
GraphQLのようなスキーマ定義言語を新たに学ぶ必要がありません。TypeScriptの知識があれば、1〜2日程度で基本的な使い方を習得できます。
tRPCが適したプロジェクト
向いているプロジェクト
- 社内業務システム(顧客管理、案件管理、タスク管理など)
- 管理画面(ECサイトの管理画面、予約システムの管理画面など)
- TypeScriptフルスタックプロジェクト(Next.jsなど)
- 小〜中規模のWebアプリケーション
向いていないケース
- 外部に公開するパブリックAPI(REST APIの方が標準的)
- モバイルアプリなど非TypeScript環境との連携
- マイクロサービス間の通信(gRPCなど専用のRPCフレームワークが適切)
tRPCの基礎知識と環境構築
必要な技術スタック
tRPCを使い始めるために必要な技術要件は以下の通りです。
必須の技術スタック
- TypeScript:バージョン4.5以上
- Node.js:バージョン14以上(LTS版を推奨)
- npm または yarn:パッケージ管理ツール
推奨される知識レベル
- TypeScriptの基本文法(型定義、インターフェース)
- 非同期処理(async/await)の理解
- Node.jsの基礎知識
基本的な型定義ができれば十分スタート可能です。tRPCの型推論が多くを自動的に処理してくれます。
tRPCの基本構成要素
tRPCは3つの主要な概念で構成されています。
Router(ルーター)
複数のAPIエンドポイント(Procedure)をまとめる役割を持ちます。
const appRouter = router({
user: userRouter, // ユーザー関連のAPI
product: productRouter, // 商品関連のAPI
});
Procedure(プロシージャ)
実際のAPI処理を定義する単位です。
- Query:データ取得(GETに相当)
- Mutation:データ更新・作成・削除(POST、PUT、DELETEに相当)
Context(コンテキスト)
すべてのProcedureで共有される情報(データベース接続、認証情報など)を格納します。
型共有の仕組み
tRPCの型共有は以下のように実現されます。
- サーバー側でRouterを定義し、型情報を
AppRouter型としてエクスポート - クライアント側で
AppRouter型をインポート - tRPCクライアントが型情報を参照し、自動的に型補完が効く
重要なのは、コンパイル時に型情報が共有されるという点です。追加のネットワーク通信やランタイムオーバーヘッドなしに型安全性を実現しています。
// サーバー側
export type AppRouter = typeof appRouter;
// クライアント側
import type { AppRouter } from './server';
const trpc = createTRPCClient<AppRouter>({ ... });
環境構築の手順
基本パッケージのインストール
# 新規プロジェクトの作成
mkdir my-trpc-project
cd my-trpc-project
npm init -y
# TypeScriptのインストール
npm install -D typescript @types/node
npx tsc --init
# tRPC本体とバリデーション用Zodのインストール
npm install @trpc/server @trpc/client zod
Next.jsプロジェクトの場合
# Next.js用の追加パッケージ
npm install @trpc/next @trpc/react-query @tanstack/react-query
tsconfig.jsonの設定
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
この作業は30分〜1時間程度で完了します。
tRPCの使い方|基本的な実装手順
実際にコードを書いて、tRPCの基本的な使い方を理解しましょう。
サーバー側の実装
tRPCインスタンスの初期化
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
Routerの作成
// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
const users = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com' },
{ id: 2, name: '佐藤花子', email: 'sato@example.com' },
];
export const userRouter = router({
// ユーザー一覧取得
list: publicProcedure.query(() => {
return users;
}),
// IDでユーザー取得
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(({ input }) => {
const user = users.find(u => u.id === input.id);
if (!user) throw new Error('ユーザーが見つかりません');
return user;
}),
});
メインRouterの作成
// server/routers/_app.ts
import { router } from '../trpc';
import { userRouter } from './user';
export const appRouter = router({
user: userRouter,
});
export type AppRouter = typeof appRouter;
このAppRouter型をクライアント側で使用することで、型安全性が実現されます。
クライアント側の実装
tRPCクライアントの作成
// client/trpc.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/routers/_app';
export const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
});
React(Next.js)での使用
// pages/_app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/routers/_app';
export const trpc = createTRPCReact<AppRouter>();
function MyApp({ Component, pageProps }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [httpBatchLink({ url: '/api/trpc' })],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
</trpc.Provider>
);
}
Query(データ取得)の実装
// pages/users.tsx
import { trpc } from '../utils/trpc';
export default function UsersPage() {
const { data, isLoading, error } = trpc.user.list.useQuery();
if (isLoading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error.message}</div>;
return (
<div>
<h1>ユーザー一覧</h1>
<ul>
{data?.map(user => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
);
}
エディタ上で自動補完が効き、存在しないプロパティにアクセスしようとするとエラーが表示されます。
Mutation(データ更新)の実装
サーバー側
export const userRouter = router({
// ユーザー作成
create: publicProcedure
.input(z.object({
name: z.string().min(1, '名前は必須です'),
email: z.string().email('有効なメールアドレスを入力してください'),
}))
.mutation(({ input }) => {
const newUser = {
id: users.length + 1,
name: input.name,
email: input.email,
};
users.push(newUser);
return newUser;
}),
});
クライアント側
export default function UserForm() {
const utils = trpc.useContext();
const createUser = trpc.user.create.useMutation({
onSuccess: () => {
utils.user.list.invalidate(); // キャッシュを無効化
},
});
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await createUser.mutateAsync({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="名前" required />
<input name="email" type="email" placeholder="メール" required />
<button type="submit" disabled={createUser.isLoading}>
{createUser.isLoading ? '作成中...' : 'ユーザー作成'}
</button>
</form>
);
}
Next.jsでtRPCを使う実践方法
Next.jsプロジェクトへの導入
Next.jsプロジェクトにtRPCを導入する最も簡単な方法は、Create T3 Appを使用することです。
npm create t3-app@latest
対話形式で必要なパッケージ(tRPC、Prisma、NextAuthなど)を選択するだけで、ベストプラクティスに沿った構成が自動的に作成されます。
プロジェクト構成例
src/
├── pages/
│ ├── api/
│ │ └── trpc/
│ │ └── [trpc].ts # tRPCのAPIハンドラー
│ └── _app.tsx # tRPC Providerの設定
├── server/
│ ├── routers/
│ │ ├── _app.ts # メインRouter
│ │ └── user.ts # ユーザー関連Router
│ └── trpc.ts # tRPC初期化
└── utils/
└── trpc.ts # クライアント設定
Pages RouterとApp Routerでの設定
Pages Router(従来の方式)
// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';
export default createNextApiHandler({
router: appRouter,
createContext: () => ({}),
});
App Router(Next.js 13+)
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '../../../../server/routers/_app';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => ({}),
});
export { handler as GET, handler as POST };
型安全なAPI開発の実践テクニック
Zodを使った入力バリデーション
Zodは、TypeScriptの型定義とランタイムバリデーションを統合するライブラリです。
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(1, '名前は必須です').max(50, '名前は50文字以内です'),
email: z.string().email('有効なメールアドレスを入力してください'),
age: z.number().min(0).max(150).optional(),
});
export const userRouter = router({
create: publicProcedure
.input(userSchema)
.mutation(({ input }) => {
// inputは自動的に型推論される
return createUser(input);
}),
});
ミドルウェアで認証を実装
// server/trpc.ts
const t = initTRPC.context<Context>().create();
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
session: ctx.session,
},
});
});
export const protectedProcedure = t.procedure.use(isAuthed);
// 認証が必要なAPI
export const userRouter = router({
getProfile: protectedProcedure.query(({ ctx }) => {
return getUserProfile(ctx.session.user.id);
}),
});
エラーハンドリング
import { TRPCError } from '@trpc/server';
export const userRouter = router({
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(({ input }) => {
const user = users.find(u => u.id === input.id);
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'ユーザーが見つかりません',
});
}
return user;
}),
});
クライアント側でのエラーハンドリング:
const { data, error } = trpc.user.getById.useQuery({ id: 1 });
if (error) {
if (error.data?.code === 'NOT_FOUND') {
return <div>ユーザーが見つかりません</div>;
}
return <div>エラーが発生しました</div>;
}
tRPC導入時の注意点と対処法
パフォーマンス最適化
バッチ処理の活用
tRPCは複数のリクエストを自動的にバッチ処理します。httpBatchLinkを使用することで、ネットワーク通信を最適化できます。
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: '/api/trpc',
maxURLLength: 2083, // URLの最大長
}),
],
});
キャッシュの活用
React Queryのキャッシュ機能を活用し、不要なリクエストを減らします。
const { data } = trpc.user.list.useQuery(undefined, {
staleTime: 60 * 1000, // 1分間はキャッシュを使用
});
既存プロジェクトへの段階的な導入
既存のREST APIプロジェクトにtRPCを導入する場合は、段階的に移行することをおすすめします。
- 新規機能からtRPCで実装
- 既存APIのうち、変更頻度の高いものから順次移行
- 安定したAPIは最後に移行
REST APIとtRPCを並行して運用することも可能です。
よくあるエラーと対処法
型が合わないエラー
サーバー側の型定義を確認し、Zodスキーマと実際のデータ構造が一致しているか確認してください。
useQueryが未定義
createTRPCReactを使用しているか、Providerの設定を見直してください。
データが更新されない
Mutation成功時にinvalidate()でキャッシュを無効化してください。
const createUser = trpc.user.create.useMutation({
onSuccess: () => {
utils.user.list.invalidate();
},
});
まとめ:tRPCで型安全なAPI開発を始めよう
tRPCで得られる具体的なメリット
tRPCを導入することで、以下のメリットが得られます。
- 開発効率の向上:API仕様書の作成・更新が不要になり、API関連の作業時間を30〜50%削減
- バグの早期発見:型の不一致をコンパイル時に検出し、本番環境でのエラーを削減
- 優れた開発体験:エディタの補完機能がフルに活用でき、タイプミスや間違った引数を即座に検出
- 学習コストの低さ:TypeScriptの知識があれば1〜2日程度で習得可能
中小企業のシステム開発におけるtRPCの活用
中小企業の業務システム開発では、限られたIT人材で多くの業務をこなす必要があります。tRPCを導入することで、必要最小限の機能でシンプルに使える業務システムを短期間・低コストで構築できます。
特に社内向けの顧客管理、案件管理、タスク管理システムなどでは、tRPCの適用範囲は非常に広く、開発期間とコストを抑えられる強力な選択肢となります。
次のステップ
tRPCをさらに学ぶためのリソース:
- 公式ドキュメント:https://trpc.io/
- Create T3 App:https://create.t3.gg/
- tRPC Examples:GitHubで実践的なサンプルコードを参照
まずは小さなプロジェクトから始めて、tRPCの型安全なAPI開発を体験してみてください。TypeScriptの知識があれば、すぐに使い始められます。