T-CREATOR

tRPC チートシート:Router/Procedure/ctx/useQuery/useMutation 早見表

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

各要素の役割を理解しておくと、実装がスムーズになります。

#要素役割
1Router複数の Procedure をまとめ、エンドポイントとして公開する
2Procedure具体的な API 処理(query/mutation)を定義する
3Context (ctx)認証情報やデータベース接続など、共通で利用する情報を格納する
4Client (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 の主要な概念を コピー&ペーストですぐ使える形式 で整理します。

  1. Router の定義(基本形・ネスト形)
  2. Procedure の定義(query/mutation)
  3. Context (ctx) の定義と利用
  4. Client での呼び出し(useQuery/useMutation)
  5. エラーハンドリングとバリデーション

それぞれについて、表形式での比較最小限のコード例 をセットで記載します。 開発中に迷ったときは、該当する見出しを探すだけで必要な情報がすぐに見つかります。

具体例

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.getUsertrpc.post.getPosts のように呼び出せます。

Router 定義のパターン比較表

#パターン用途
1単一 Router小規模プロジェクトappRouter = t.router({ ... })
2ネスト Router機能ごとに分割appRouter = t.router({ user: userRouter, post: postRouter })
3merge による統合既存 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 メソッド相当
1queryデータの取得(読み取り)GETユーザー情報取得、投稿一覧取得
2mutationデータの作成・更新・削除(書き込み)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)

フロントエンドでは、useQueryuseMutation を使って 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 オプション早見表

#オプション説明
1enabledクエリを実行する条件enabled: !!userId
2onSuccess成功時のコールバックonSuccess: (data) => {...}
3onErrorエラー時のコールバックonError: (error) => {...}
4refetchInterval自動再取得の間隔(ミリ秒)refetchInterval: 5000
5staleTimeデータの有効期限(ミリ秒)staleTime: 60000
6retry失敗時のリトライ回数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 オプション早見表

#オプション説明
1onSuccess成功時のコールバックonSuccess: (data) => {...}
2onErrorエラー時のコールバックonError: (error) => {...}
3onMutate実行前のコールバックonMutate: (variables) => {...}
4onSettled成功・失敗に関わらず実行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 ステータス用途
1BAD_REQUEST400リクエストが不正
2UNAUTHORIZED401認証が必要
3FORBIDDEN403権限がない
4NOT_FOUND404リソースが見つからない
5TIMEOUT408タイムアウト
6CONFLICT409リソースの競合
7PRECONDITION_FAILED412前提条件が満たされていない
8PAYLOAD_TOO_LARGE413リクエストが大きすぎる
9INTERNAL_SERVER_ERROR500サーバーエラー

フロントエンド側でのエラーハンドリング

クライアント側では、エラーを 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 の型安全な開発を、このチートシートでさらにスピードアップさせていきましょう。

関連リンク