tRPC チートシート:Router/Procedure/ctx/useQuery/useMutation 早見表
tRPC を実装する際、Router や Procedure、ctx、useQuery、useMutation といった概念を素早く参照したいことがありますよね。この記事は、tRPC の主要な構成要素を すぐに使える形 でまとめたチートシート(早見表)です。 開発中に「あの書き方、どうだったかな?」と迷ったとき、この記事をサッと開けば必要な情報がすぐに見つかります。
背景
tRPC の主要概念とは
tRPC は 型安全な API 通信 を実現するフレームワークですが、実装にはいくつかの重要な概念があります。
以下の図は、tRPC における主要な構成要素の関係性を示しています。
mermaidflowchart TB
router["Router<br/>(エンドポイント集約)"]
procedure["Procedure<br/>(query/mutation)"]
ctx["Context (ctx)<br/>(共通データ・認証情報)"]
client["Client<br/>(useQuery/useMutation)"]
router -->|含む| procedure
ctx -->|提供| procedure
procedure -->|呼び出し| client
各要素の役割を理解しておくと、実装がスムーズになります。
| # | 要素 | 役割 |
|---|---|---|
| 1 | Router | 複数の Procedure をまとめ、エンドポイントとして公開する |
| 2 | Procedure | 具体的な API 処理(query/mutation)を定義する |
| 3 | Context (ctx) | 認証情報やデータベース接続など、共通で利用する情報を格納する |
| 4 | Client (useQuery/useMutation) | フロントエンドから tRPC を呼び出すための React Hooks |
この記事では、これらの要素を コードと表で素早く参照できる形式 でまとめました。
課題
よくある「忘れがち」な実装パターン
tRPC を使っていると、以下のような疑問が頻繁に出てきます。
- Router の定義方法と、ネストした Router の書き方は?
- query と mutation の使い分けと書き方の違いは?
- ctx にはどうやってデータを渡すの?認証情報はどう扱う?
- useQuery と useMutation のオプション(enabled、onSuccess など)は何があるの?
- エラーハンドリングはどう書くの?
これらを毎回ドキュメントで調べるのは時間がかかりますし、記憶に頼ると間違えることもありますよね。 すぐに参照できるチートシート があれば、開発効率が大きく向上します。
以下の図は、実装時によくある迷いポイントを示しています。
mermaidflowchart LR
dev["開発者"]
q1["Router の<br/>ネスト方法は?"]
q2["ctx の<br/>初期化は?"]
q3["useQuery の<br/>オプションは?"]
q4["エラー<br/>ハンドリングは?"]
dev --> q1
dev --> q2
dev --> q3
dev --> q4
これらの疑問を解決するために、実用的なコード例と表形式でまとめたチートシートを用意しました。
解決策
チートシート形式でまとめた実装パターン
この記事では、以下の順序で tRPC の主要な概念を コピー&ペーストですぐ使える形式 で整理します。
- Router の定義(基本形・ネスト形)
- Procedure の定義(query/mutation)
- Context (ctx) の定義と利用
- Client での呼び出し(useQuery/useMutation)
- エラーハンドリングとバリデーション
それぞれについて、表形式での比較 と 最小限のコード例 をセットで記載します。 開発中に迷ったときは、該当する見出しを探すだけで必要な情報がすぐに見つかります。
具体例
Router の定義
Router は、複数の Procedure をまとめてエンドポイントとして公開する役割を持ちます。
基本的な Router の作成
まず、tRPC の Router を初期化します。
typescriptimport { initTRPC } from '@trpc/server';
// tRPC インスタンスを初期化
const t = initTRPC.create();
次に、Router を定義します。
typescript// Router の作成
export const appRouter = t.router({
// ここに Procedure を定義していく
});
// Router の型をエクスポート(型安全のため)
export type AppRouter = typeof appRouter;
Router のネスト(複数の Router を統合)
大規模なアプリケーションでは、Router をネストして整理します。
typescript// ユーザー関連の Router
const userRouter = t.router({
getUser: t.procedure.query(() => {
return { id: 1, name: 'Taro' };
}),
});
typescript// 投稿関連の Router
const postRouter = t.router({
getPosts: t.procedure.query(() => {
return [{ id: 1, title: 'Hello tRPC' }];
}),
});
typescript// メインの Router にネスト
export const appRouter = t.router({
user: userRouter,
post: postRouter,
});
export type AppRouter = typeof appRouter;
フロントエンドからは trpc.user.getUser や trpc.post.getPosts のように呼び出せます。
Router 定義のパターン比較表
| # | パターン | 用途 | 例 |
|---|---|---|---|
| 1 | 単一 Router | 小規模プロジェクト | appRouter = t.router({ ... }) |
| 2 | ネスト Router | 機能ごとに分割 | appRouter = t.router({ user: userRouter, post: postRouter }) |
| 3 | merge による統合 | 既存 Router を結合 | t.mergeRouters(userRouter, postRouter) |
Procedure の定義(query/mutation)
Procedure は、実際の API 処理を定義します。query(読み取り)と mutation(書き込み)の 2 種類があります。
query(読み取り処理)
データを取得する処理には query を使います。
typescriptimport { z } from 'zod';
// ユーザー情報を取得する query
const getUserById = t.procedure
// input でバリデーションを定義
.input(z.object({ id: z.number() }))
.query(({ input }) => {
// input には型安全に { id: number } が渡される
return { id: input.id, name: 'Taro' };
});
Router に組み込む場合は以下のようになります。
typescriptexport const appRouter = t.router({
getUserById,
});
mutation(書き込み処理)
データを作成・更新・削除する処理には mutation を使います。
typescript// ユーザーを作成する mutation
const createUser = t.procedure
.input(z.object({ name: z.string() }))
.mutation(({ input }) => {
// データベースへの書き込み処理
return { id: 1, name: input.name };
});
typescriptexport const appRouter = t.router({
createUser,
});
query と mutation の比較表
| # | 種類 | 用途 | HTTP メソッド相当 | 例 |
|---|---|---|---|---|
| 1 | query | データの取得(読み取り) | GET | ユーザー情報取得、投稿一覧取得 |
| 2 | mutation | データの作成・更新・削除(書き込み) | POST/PUT/DELETE | ユーザー作成、投稿削除 |
input と output の型推論
tRPC では、input と output の型が自動的に推論されます。
typescript// Zod スキーマで input を定義
const updateUser = t.procedure
.input(
z.object({
id: z.number(),
name: z.string(),
})
)
.mutation(({ input }) => {
// input.id, input.name が型安全に利用できる
return { success: true };
});
フロントエンドでは、引数の型が自動的に補完されます。
Context (ctx) の定義と利用
Context(ctx)は、すべての Procedure で共通して利用できるデータを格納する仕組みです。 認証情報やデータベース接続、セッション情報などを渡すのに便利です。
Context の初期化
Context を定義して、tRPC インスタンスに渡します。
typescriptimport { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
// Context の作成関数
export const createContext = async (
opts: CreateNextContextOptions
) => {
// リクエストごとに実行される
return {
// 認証情報などをここに含める
userId: 1, // 仮の値(実際は認証処理で取得)
};
};
typescript// Context の型を推論
export type Context = inferAsyncReturnType<
typeof createContext
>;
typescript// tRPC インスタンスに Context の型を渡す
const t = initTRPC.context<Context>().create();
Procedure 内で ctx を利用
Procedure 内では、ctx を通じて Context にアクセスできます。
typescriptconst getMyProfile = t.procedure.query(({ ctx }) => {
// ctx.userId にアクセスできる
return { userId: ctx.userId, name: 'Taro' };
});
認証チェックのミドルウェア
Context を使って、認証が必要な Procedure を保護できます。
typescriptimport { TRPCError } from '@trpc/server';
// 認証済みユーザー専用のミドルウェア
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.userId) {
// 認証されていない場合はエラーを投げる
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
// 認証済みユーザーの情報を渡す
userId: ctx.userId,
},
});
});
typescript// 認証済み Procedure の作成
const protectedProcedure = t.procedure.use(isAuthed);
typescript// 認証が必要な Procedure
const deletePost = protectedProcedure
.input(z.object({ postId: z.number() }))
.mutation(({ ctx, input }) => {
// ctx.userId が型安全に利用できる
return { success: true };
});
Context 活用パターン表
| # | パターン | 用途 | 例 |
|---|---|---|---|
| 1 | リクエスト情報 | ヘッダー、Cookie の取得 | ctx.req.headers |
| 2 | 認証情報 | ユーザー ID、セッション | ctx.userId |
| 3 | データベース接続 | Prisma クライアントなど | ctx.prisma |
| 4 | 環境変数 | API キーなど | ctx.apiKey |
Client での呼び出し(useQuery/useMutation)
フロントエンドでは、useQuery と useMutation を使って tRPC の Procedure を呼び出します。
useQuery(query の呼び出し)
データを取得する場合は useQuery を使います。
typescriptimport { trpc } from '@/utils/trpc';
function UserProfile() {
// getUserById を呼び出し
const { data, isLoading, error } =
trpc.getUserById.useQuery({ id: 1 });
if (isLoading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error.message}</div>;
return <div>{data?.name}</div>;
}
useQuery のオプション
useQuery には便利なオプションがあります。
typescriptconst { data } = trpc.getUserById.useQuery(
{ id: 1 },
{
// 条件付きで実行(id がある場合のみ)
enabled: !!userId,
// データ取得成功時のコールバック
onSuccess: (data) => {
console.log('取得成功:', data);
},
// エラー時のコールバック
onError: (error) => {
console.error('取得失敗:', error);
},
// 再取得の間隔(ミリ秒)
refetchInterval: 5000,
// 古いデータの有効期限(ミリ秒)
staleTime: 60000,
}
);
useQuery オプション早見表
| # | オプション | 説明 | 例 |
|---|---|---|---|
| 1 | enabled | クエリを実行する条件 | enabled: !!userId |
| 2 | onSuccess | 成功時のコールバック | onSuccess: (data) => {...} |
| 3 | onError | エラー時のコールバック | onError: (error) => {...} |
| 4 | refetchInterval | 自動再取得の間隔(ミリ秒) | refetchInterval: 5000 |
| 5 | staleTime | データの有効期限(ミリ秒) | staleTime: 60000 |
| 6 | retry | 失敗時のリトライ回数 | retry: 3 |
useMutation(mutation の呼び出し)
データを作成・更新・削除する場合は useMutation を使います。
typescriptfunction CreateUser() {
const mutation = trpc.createUser.useMutation();
const handleCreate = () => {
// mutation を実行
mutation.mutate({ name: 'Hanako' });
};
return (
<button
onClick={handleCreate}
disabled={mutation.isLoading}
>
ユーザー作成
</button>
);
}
useMutation のオプション
useMutation にもコールバックやエラーハンドリングのオプションがあります。
typescriptconst mutation = trpc.createUser.useMutation({
// 成功時の処理
onSuccess: (data) => {
console.log('作成成功:', data);
// クエリの再取得
trpcUtils.getUserById.invalidate();
},
// エラー時の処理
onError: (error) => {
console.error('作成失敗:', error);
},
// 実行前の処理
onMutate: (variables) => {
console.log('実行開始:', variables);
},
});
typescript// mutate の引数でもコールバックを指定できる
mutation.mutate(
{ name: 'Hanako' },
{
onSuccess: (data) => {
console.log('個別の成功処理:', data);
},
}
);
useMutation オプション早見表
| # | オプション | 説明 | 例 |
|---|---|---|---|
| 1 | onSuccess | 成功時のコールバック | onSuccess: (data) => {...} |
| 2 | onError | エラー時のコールバック | onError: (error) => {...} |
| 3 | onMutate | 実行前のコールバック | onMutate: (variables) => {...} |
| 4 | onSettled | 成功・失敗に関わらず実行 | onSettled: (data, error) => {...} |
キャッシュの無効化(invalidate)
データを更新した後、関連する query のキャッシュを無効化して再取得させることができます。
typescriptimport { trpc } from '@/utils/trpc';
function UpdateUser() {
const trpcUtils = trpc.useContext();
const mutation = trpc.updateUser.useMutation({
onSuccess: () => {
// ユーザー情報の再取得
trpcUtils.getUserById.invalidate();
},
});
return (
<button
onClick={() =>
mutation.mutate({ id: 1, name: 'Updated' })
}
>
更新
</button>
);
}
エラーハンドリングとバリデーション
tRPC では、Zod によるバリデーションと、統一されたエラーハンドリングが可能です。
Zod によるバリデーション
入力値のバリデーションは、Zod スキーマで定義します。
typescriptimport { z } from 'zod';
const createPost = t.procedure
.input(
z.object({
// 文字列、1文字以上100文字以下
title: z.string().min(1).max(100),
// 文字列、オプショナル
content: z.string().optional(),
// 列挙型
status: z.enum(['draft', 'published']),
})
)
.mutation(({ input }) => {
// input は型安全にバリデーション済み
return { id: 1, ...input };
});
サーバー側でのエラー送出
Procedure 内でエラーを送出する場合は、TRPCError を使います。
typescriptimport { TRPCError } from '@trpc/server';
const deletePost = t.procedure
.input(z.object({ postId: z.number() }))
.mutation(({ input, ctx }) => {
// 権限チェック
if (!ctx.userId) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'ログインが必要です',
});
}
// 存在チェック
const post = findPost(input.postId);
if (!post) {
throw new TRPCError({
code: 'NOT_FOUND',
message: '投稿が見つかりません',
});
}
return { success: true };
});
tRPC エラーコード早見表
| # | コード | HTTP ステータス | 用途 |
|---|---|---|---|
| 1 | BAD_REQUEST | 400 | リクエストが不正 |
| 2 | UNAUTHORIZED | 401 | 認証が必要 |
| 3 | FORBIDDEN | 403 | 権限がない |
| 4 | NOT_FOUND | 404 | リソースが見つからない |
| 5 | TIMEOUT | 408 | タイムアウト |
| 6 | CONFLICT | 409 | リソースの競合 |
| 7 | PRECONDITION_FAILED | 412 | 前提条件が満たされていない |
| 8 | PAYLOAD_TOO_LARGE | 413 | リクエストが大きすぎる |
| 9 | INTERNAL_SERVER_ERROR | 500 | サーバーエラー |
フロントエンド側でのエラーハンドリング
クライアント側では、エラーを error プロパティで取得できます。
typescriptfunction DeletePost({ postId }: { postId: number }) {
const mutation = trpc.deletePost.useMutation();
const handleDelete = () => {
mutation.mutate(
{ postId },
{
onError: (error) => {
// エラーコードとメッセージを取得
console.error('エラーコード:', error.data?.code);
console.error('メッセージ:', error.message);
// エラーコードに応じた処理
if (error.data?.code === 'UNAUTHORIZED') {
alert('ログインしてください');
}
},
}
);
};
return (
<>
<button onClick={handleDelete}>削除</button>
{mutation.error && (
<div>エラー: {mutation.error.message}</div>
)}
</>
);
}
バリデーションエラーの取得
Zod のバリデーションエラーは、詳細な情報を含んでいます。
typescriptconst mutation = trpc.createPost.useMutation({
onError: (error) => {
// Zod のバリデーションエラー
if (error.data?.zodError) {
const fieldErrors = error.data.zodError.fieldErrors;
console.log('フィールドエラー:', fieldErrors);
// { title: ['1文字以上必要です'], ... }
}
},
});
実装パターンの全体図
最後に、これまでの要素がどのように連携するかを図で示します。
mermaidsequenceDiagram
participant Client as フロントエンド<br/>(useQuery/useMutation)
participant Router as Router
participant Procedure as Procedure<br/>(query/mutation)
participant ctx as Context (ctx)
participant DB as データベース
Client->>Router: API 呼び出し
Router->>Procedure: 該当 Procedure へ
Procedure->>ctx: Context から<br/>認証情報取得
ctx-->>Procedure: userId など
Procedure->>DB: データ操作
DB-->>Procedure: 結果
Procedure-->>Router: レスポンス
Router-->>Client: 型安全なデータ
この図から、各要素の役割と処理の流れが理解できますね。
まとめ
この記事では、tRPC の主要な構成要素を チートシート形式 でまとめました。
以下の内容を整理しました。
- Router の定義: 基本形とネスト形、統合パターン
- Procedure の定義: query と mutation の書き方と使い分け
- Context (ctx): 共通データの渡し方と認証ミドルウェア
- Client での呼び出し: useQuery と useMutation のオプションと活用方法
- エラーハンドリング: TRPCError とバリデーションエラーの扱い方
開発中に「あの書き方、どうだったかな?」と迷ったときは、この記事の該当する見出しや表を参照してください。 コードをコピー&ペーストすれば、すぐに実装できる形式でまとめてあります。
tRPC の型安全な開発を、このチートシートでさらにスピードアップさせていきましょう。
関連リンク
articletRPC チートシート:Router/Procedure/ctx/useQuery/useMutation 早見表
articletRPC の始め方:Next.js App Router と Zod を使った最小構成テンプレート
articletRPC とは?型安全なフルスタック通信を実現する仕組みとメリット【2025 年版】
articleReact クリーンアーキテクチャ実践:UI・アプリ・ドメイン・データの責務分離
articleWebLLM vs サーバー推論 徹底比較:レイテンシ・コスト・スケールの実測レポート
articleVitest モック技術比較:MSW / `vi.mock` / 手動スタブ — API テストの最適解はどれ?
articlePython ORMs 実力検証:SQLAlchemy vs Tortoise vs Beanie の選び方
articleVite で Web Worker / SharedWorker を TypeScript でバンドルする初期設定
articlePrisma Accelerate と PgBouncer を比較:サーバレス時代の接続戦略ベンチ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来