tRPC の使い方 — TypeScriptファーストなRPC APIツールキット
一言でいうと
tRPCは、TypeScriptの型システムを最大限に活用し、サーバーとクライアント間でエンドツーエンドの型安全性を実現するRPCフレームワークです。APIスキーマ定義やコード生成なしに、サーバーの関数をクライアントから型付きで呼び出せます。
⚠️ 重要な注意: この記事で解説する
trpcパッケージ(v0.11.3)は、現在広く使われている@trpc/server/@trpc/client(v10/v11系)とは別のパッケージです。現在のtRPCエコシステムは@trpc/*スコープに移行しており、このtrpcパッケージは初期の実験的バージョンです。新規プロジェクトでは@trpc/serverの利用を推奨します。
どんな時に使う?
- フルスタックTypeScriptアプリケーション — Next.jsやモノレポ構成で、サーバーとクライアントのコードを同一リポジトリで管理し、APIの型をシームレスに共有したい場合
- REST/GraphQLのボイラープレートを排除したい — スキーマ定義ファイルやコード生成ステップなしに、サーバーの関数をそのままクライアントから呼び出したい場合
- サーバー内部でのAPI再利用 — マイクロサービス間やサーバーサイドのロジックで、自分のAPIを型安全に呼び出すServer SDKが必要な場合
インストール
# npm
npm install trpc
# yarn
yarn add trpc
# pnpm
pnpm add trpc
動作要件:
- TypeScript 3.2以上
- Proxy API対応環境(IE11非対応)
基本的な使い方
最もシンプルなパターンとして、エンドポイント定義 → ルーター構築 → リクエスト処理の流れを示します。
import { trpc } from 'trpc';
// 1. エンドポイントを定義
const greet = trpc.endpoint(() => (name: string) => {
return `Hello, ${name}!`;
});
// 2. ルーターを構築
const rootRouter = trpc.router().endpoint('greet', greet);
// 3. リクエストを処理
const result = await rootRouter.handle({
endpoint: ['greet'],
context: {},
args: ['World'],
});
// => "Hello, World!"
よく使うAPI
1. trpc.endpoint() — エンドポイントの定義
「関数を返す関数」を受け取ります。外側の関数がコンテキスト(認証情報など)を受け取り、内側の関数が実際のビジネスロジックです。
type Ctx = { token: string };
// コンテキストなしのシンプルなエンドポイント
const computeLength = trpc.endpoint(() => (data: string) => {
return data.length;
});
// コンテキストありのエンドポイント
const getProfile = trpc.endpoint((ctx: Ctx) => async () => {
const user = await getUserFromToken(ctx.token);
return { name: user.name, email: user.email };
});
// 引数とコンテキスト両方を使うエンドポイント
const postTweet = trpc.endpoint((ctx: Ctx) => async (data: { content: string }) => {
const user = await getUserFromToken(ctx.token);
const newTweet = await createTweet({
content: data.content,
userId: user.id,
});
return newTweet;
});
2. .authorize() — 認可ロジックの追加
デフォルトではすべてのリクエストが拒否されます。 .authorize() で明示的に許可条件を定義する必要があります。
const getUsername = trpc
.endpoint((ctx: Ctx) => async () => {
const user = await getUserFromToken(ctx.token);
return user.username;
})
.authorize((ctx) => async (_data) => {
if (!ctx.token) return false;
return true;
});
3. trpc.router() / .endpoint() / .compose() — ルーターの構築と合成
ルーターはイミュータブルです。必ずメソッドチェーンで構築してください。
// エンドポイントの登録(チェーン必須)
const userRouter = trpc
.router()
.endpoint('getUsername', getUsername)
.endpoint('postTweet', postTweet);
// ルーターの階層化
const rootRouter = trpc
.router()
.endpoint('computeLength', computeLength)
.compose('user', userRouter);
// ⛔ これは動かない!ルーターはイミュータブル
// const router = trpc.router();
// router.endpoint('a', endpointA);
// router.endpoint('b', endpointB);
4. router.toServerSDK() — サーバーサイドSDKの生成
サーバー内部でAPIを型安全に呼び出すためのSDKを自動生成します。認可チェックを自動的にバイパスするため、クライアントに露出させないよう注意が必要です。
const serverSDK = rootRouter.toServerSDK();
// 第1引数は常にコンテキスト
const length = serverSDK.computeLength({}, 'hello'); // => 5
// ネストしたルーターのエンドポイントも呼べる
const username = await serverSDK.user.getUsername({ token: 'SAMPLE_TOKEN' });
const newTweet = await serverSDK.user.postTweet(
{ token: 'SAMPLE_TOKEN' },
{ content: 'just setting up my twttr' },
);
5. trpc.sdk() — クライアントSDKの生成
サーバーのルーター型を型のみインポートし、クライアントSDKを生成します。
// client.ts
import { trpc } from 'trpc';
// 型だけをインポート(実際のサーバーコードはバンドルされない)
type RootRouter = typeof import('./server').rootRouter;
const clientSDK = trpc.sdk<RootRouter>({
url: 'http://localhost:3000/rpc',
getContext: () => {
const token = getTokenFromCookies();
return { token };
},
});
// SDK呼び出しはTRPCResult<T>を同期的に返す
const query = clientSDK.computeLength('asdf');
// .run()で実際にサーバーへリクエストを送信
const result = await query.run();
// => 4
TRPCResult<T> の構造:
type TRPCResult<T> = {
run: () => Promise<T>;
payload: {
path: string[];
args: unknown[];
context: unknown;
};
};
Express連携
import express from 'express';
import bodyParser from 'body-parser';
const app = express();
app.use(bodyParser.json());
// 単一エンドポイントでAPI全体を公開(GraphQLと同様のパターン)
app.post('/rpc', rootRouter.toExpress());
app.listen(3000);
類似パッケージとの比較
| 特徴 | trpc (v0.11) | @trpc/server (v10/v11) | GraphQL (Apollo等) | REST (Express等) |
|---|---|---|---|---|
| 型安全性 | ✅ エンドツーエンド | ✅ エンドツーエンド | ⚠️ コード生成が必要 | ❌ 手動定義 |
| スキーマ定義 | 不要 | 不要 | 必要 | 不要(OpenAPI等は別途) |
| コード生成 | 不要 | 不要 | 必要 | 必要(型安全にする場合) |
| エコシステム | ⚠️ 実験的・非推奨 | ✅ 活発・React/Next.js統合 | ✅ 成熟 | ✅ 成熟 |
| 学習コスト | 低 | 中 | 高 | 低 |
| バリデーション | なし | Zod等と統合 | スキーマで定義 | 手動 |
| サブスクリプション | ❌ | ✅ | ✅ | ❌(WebSocket別途) |
結論: 新規プロジェクトでtRPCのアプローチを採用するなら、@trpc/server + @trpc/client を使うべきです。
注意点・Tips
🚨 ルーターはイミュータブル
最もハマりやすいポイントです。.endpoint() は新しいルーターを返すため、必ずチェーンするか戻り値を変数に代入してください。
// ✅ 正しい
const router = trpc.router()
.endpoint('a', endpointA)
.endpoint('b', endpointB);
// ❌ 間違い(endpointBしか登録されない、ではなく、そもそも型が合わない)
const router = trpc.router();
router.endpoint('a', endpointA);
router.endpoint('b', endpointB);
🚨 デフォルトで全リクエスト拒否
.authorize() を付けないエンドポイントはリクエストが拒否されます。開発中に「なぜ動かないのか」と悩んだら、まず認可設定を確認してください。
🚨 Server SDKは認可をバイパスする
toServerSDK() で生成したSDKは認可チェックをスキップします。クライアントバンドルに含まれないよう、インポートパスに細心の注意を払ってください。
💡 クライアントSDKでは型のみインポートする
// ✅ 型だけインポート(サーバーコードはバンドルされない)
type RootRouter = typeof import('./server').rootRouter;
// ❌ 実体をインポートしてしまう(サーバーコードがクライアントに漏洩)
import { rootRouter } from './server';
💡 TRPCResult の遅延実行パターン
クライアントSDKの呼び出しは即座にリクエストを送信しません。.run() を呼ぶまでリクエストは遅延されます。これにより、ペイロードの検査やバッチ処理が可能になります。
const query = clientSDK.computeLength('test');
console.log(query.payload); // リクエスト内容を事前に確認できる
const result = await query.run(); // ここで初めてHTTPリクエストが発生
まとめ
trpc(v0.11)は、TypeScriptの型推論を活用してサーバー・クライアント間のAPI型安全性を実現する先駆的なパッケージです。エンドポイント定義・ルーター構築・SDK自動生成というシンプルな3ステップで、スキーマ定義やコード生成なしに型付きAPIを構築できます。
ただし、このパッケージは実験的な初期バージョンであり、現在は @trpc/server / @trpc/client(v10/v11系)へと進化しています。新規プロジェクトでは @trpc/* スコープのパッケージを採用し、Zodによるバリデーション統合やReact Query連携など、成熟したエコシステムの恩恵を受けることを強く推奨します。