tRPC 実践:エンドツーエンド型安全・Zod バリデーション・React Query 連携
tRPC を使う価値
従来のフロント/バック協業は「手書き API ドキュメント + 別々に管理する型」に頼っており、型を変えるとすぐにズレが生じます。tRPC の核心は:バックエンドの procedure そのものが型の出所となり、フロントから呼ぶ際に完全な型推論を得られる。codegen も schema ファイルも不要。
| 手法 | 型の出所 | codegen 要否 | 跨言語 |
|---|---|---|---|
| REST + 手書き型 | 手動管理 | いいえ | 可 |
| GraphQL + codegen | schema ファイル | はい | 可 |
| tRPC | バック手続きから直接推論 | いいえ | 不可(TS/TS のみ) |
Router と Procedure の定義
// server/trpc.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
// server/routers/user.ts
export const userRouter = router({
byId: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(({ input }) => {
return db.user.findUnique({ where: { id: input.id } });
}),
create: publicProcedure
.input(z.object({ name: z.string().min(1), email: z.string().email() }))
.mutation(({ input }) => {
return db.user.create({ data: input });
}),
});
query は読み取り、mutation は書き込みに対応。入力は Zod で宣言し、tRPC が実行時に自動検証します。
複雑な入れ子入力は、先に JSON フォーマッタ で形を整えてから Zod schema に変換すると、フィールド漏れを防げます。
React Query との統合
これが定番の組み合わせです。tRPC が型を、React Query がキャッシュ/リトライ/無効化を担います。
// client/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../server/routers";
export const trpc = createTRPCReact<AppRouter>();
function UserList() {
const users = trpc.user.list.useQuery();
const utils = trpc.useUtils();
const create = trpc.user.create.useMutation({
onSuccess: () => utils.user.list.invalidate(),
});
if (users.isLoading) return <p>読み込み中…</p>;
return (
<ul>
{users.data?.map((u) => (
<li key={u.id}>{u.name}</li>
))}
<button onClick={() => create.mutate({ name: "Ada", email: "a@x.com" })}>
追加
</button>
</ul>
);
}
users.data の型も create.mutate の引数型も、すべてバックエンド procedure から推論されます。バック入力を変えればフロントは即座にエラーになります。
ミドルウェアと認証
import { initTRPC, TRPCError } from "@trpc/server";
const t = initTRPC.context<{ jwt?: string }>().create();
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.jwt) throw new TRPCError({ code: "UNAUTHORIZED" });
return next({ ctx: { ...ctx, userId: verify(ctx.jwt) } });
});
export const protectedProcedure = t.procedure.use(isAuthed);
ミドルウェアは ctx を書き換えられ、下流 procedure は userId を受け取ります。
JWT 検証時は JWT デコード ツールで payload の
subと有効期限を確認し、認証チェーンを素早く照合できます。
エラー処理
tRPC は TRPCError でエラーを統一し、フロントは error.data.code で判別します:
protectedProcedure.query(({ ctx }) => {
const u = db.user.findUnique({ where: { id: ctx.userId } });
if (!u) throw new TRPCError({ code: "NOT_FOUND", message: "ユーザーが存在しません" });
return u;
});
フロント:
const { error } = trpc.user.me.useQuery();
if (error?.data?.code === "NOT_FOUND") return <p>見つかりません</p>;
エラーコードは HTTP ステータスコードと 1:1 対応しており、調査時は HTTP ステータスコード ツールで UNAUTHORIZED=401、NOT_FOUND=404 等を照合できます。
Subscriptions(リアルタイム)
tRPC は WebSocket ベースの購読をサポートします:
export const appRouter = router({
onMessage: publicProcedure.subscription(({ signal }) => {
return observable<Message>((emit) => {
const timer = setInterval(() => emit.next({ text: "ping" }), 1000);
return () => clearInterval(timer);
});
}),
});
REST からのスムーズな移行
- 境界を整理:「読み」→
query、「書き/副作用」→mutation。 - Zod を補完:各エンドポイントに入力 schema を付け、実行時検証を手に入れる。
- クライアントを置換:
fetch+ 手書き型をtrpc.xxx.useQuery/useMutationへ。 - 認証を下沉:散在する JWT チェックを
protectedProcedureミドルウェアに集約。 - REST を維持:tRPC は fetch adapter で HTTP エンドポイントも公開でき、段階的移行可能。
よくある質問 FAQ
Q1:tRPC は跨言語ですか?
いいえ。TypeScript の型推論に依存するため、両端とも TS である必要があります。多言語が必要なら REST/GraphQL に戻ります。
Q2:GraphQL と比べた優位性は?
schema ファイルと codegen が不要で、型がそのままソースコードになります。代償は跨言語や公開 API 能力の喪失です。
Q3:Zod 検証失敗時の戻りは?
自動的に BAD_REQUEST エラーになり、フロントは error.data.code === "BAD_REQUEST" とともに Zod 問題リストを受け取ります。
Q4:React Query を使わなくてもいいですか?
可能です。tRPC コアはクライアント非依存で、ネイティブ client 呼び出しもありますが、React Query 統合の DX が最良です。
Q5:大きな一覧のページネーションは?
input に cursor/limit を加え、query で { items, nextCursor } を返し、フロントは useInfiniteQuery と組み合わせます。
おすすめツール
tRPC フルスタック開発では、以下の ツール が役立ちます:
- JSON フォーマッタ — Zod を書く前に入出力の形を整理
- JWT デコード — 認証トークンの sub と有効期限を確認
- HTTP ステータスコード — tRPC エラーコードと HTTP 意味の照合
- Base64 エンコード — 認証情報・バイナリペイロード処理
tRPC は「また一つの RPC フレームワーク」ではなく、「型こそ契約」というパラダイムです。バックエンドの procedure がそのままフロントの型の出所となり、ドキュメントや codegen、両側でのフィールド合わせという心的負荷をなくします。バック入力を変えればエディタがフロントを即座に赤くする——そのエンドツーエンドの型安全こそが、现代フルスタック TS チームのスイートスポットです。
ブラウザローカルツールを無料で試す →