mcpField
W
web-data-fetching-trpc

web-data-fetching-trpc

github.com/agents-inc開発2662 回インストール2 日前に更新
リポジトリ

web-data-fetching-trpc について

TypeScript 開発者向けに、ルーター、プロシージャ、入力検証、クライアント側の型推論など、tRPC のパターンを解説するスキルです。v11 の連携方法やエラー処理、ミドルウェア、サブスクリプションも扱います。

主な機能

  1. 01ルーター型の共有
  2. 02プロシージャ入力の検証
  3. 03ミドルウェアによるコンテキスト型の絞り込み
  4. 04型付きクエリとミューテーションのオプション
  5. 05エラー処理とサブスクリプション

活用シーン

  1. 01エンドツーエンドで型安全な内部 API の構築
  2. 02プロシージャ入力への実行時検証の追加
  3. 03認証済みプロシージャのコンテキスト定義
  4. 04ページネーション、サブスクリプション、楽観的更新の実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/agents-inc/skills --skill web-data-fetching-trpc

インストール後、Claude Code で/web-data-fetching-trpcを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.yaml
reference.md
examples/
core.md
file-uploads.md
infinite-queries.md
middleware.md
optimistic-updates.md
subscriptions.md

詳細

tRPC のパターン

クイックガイド: tRPC は、生成されたスキーマではなく、エクスポートされた 1 つの型を通じてサーバーからクライアントへ型を引き継ぐため、export type AppRouter = typeof appRouter がその橋渡しのすべてです。これがなければ、その後の処理はすべて機能しません。プロシージャは検証済みの入力を受け取り、値を返します。TRPCError のコードは HTTP ステータスになります。ミドルウェアはコンテキストの型を絞り込むため、認証済みプロシージャの ctx.user は null になりません。v11 ではトランスフォーマーがリンク内に移動し、サブスクリプションは非同期ジェネレーターになりました。また、現在の React 統合には @trpc/tanstack-react-query を使用します。

詳細なリソース:


該当する方法

  • @trpc/tanstack-react-query — 現在の統合方法です。createTRPCContext は useTRPC フックを生成し、各プロシージャは標準のクエリフックにそのまま渡せる queryOptions()、mutationOptions()、infiniteQueryOptions() 、queryKey() を公開します。パターン 5 を参照してください。
  • @trpc/react-query — 従来の統合方法で、v11 でも引き続きサポートされています。各プロシージャが trpc.x.useQuery() フックを持ち、キャッシュキーはプロシージャの queryKey() ではなく、 getQueryKey(trpc.x) から取得します。都合のよいタイミングで移行してください。2 つの方法は併用できます。

<critical_requirements>

tRPC コードを書く前に

ルーターの型をエクスポートします:export type AppRouter = typeof appRouter。 この1行がクライアント側の契約全体です。これがないとクライアントは unknown にフォールバックし、tRPC が提供する保証がすべて失われます。エクスポートを忘れても、その箇所ではエラーになりません。

入力を受け取るすべてのプロシージャで、.input() にバリデーターを指定します。 バリデーターは実行時のチェックであり、ハンドラーのパラメーター型の元にもなるため、指定しないプロシージャは unknown を受け取り、型アサーションを使いたくなります。

単なる Error ではなく、コードを指定して TRPCError をスローします。 コードによって HTTP ステータスにマッピングされ、クライアントが処理を切り替えます。単なる Error は、詳細不明の 500 として届きます。

トランスフォーマーは createTRPCClient() ではなく、httpBatchLink() の中に配置します。 v11 で配置場所が変更され、以前の場所に置くとクライアントの作成時にエラーになります。

</critical_requirements>


自動検出: initTRPC、createTRPCClient、createTRPCContext、createTRPCOptionsProxy、@trpc/server、@trpc/client、@trpc/react-query、@trpc/tanstack-react-query、TRPCError、publicProcedure、protectedProcedure、httpBatchLink、httpSubscriptionLink、loggerLink、inferRouterInputs、inferRouterOutputs、useTRPC、tracked、AppRouter

対象:

  • ルーターとプロシージャの定義、およびルーターを組み合わせて1つにすること
  • 入力のバリデーションと、そこから推論される型
  • リクエストごとのコンテキストと、その型を絞り込むミドルウェア
  • エラーコードと、クライアントが受け取る形式
  • プロシージャを型付きのクエリーおよびミューテーションのオプションに変換すること
  • サブスクリプション、ファイル入力、カーソルページネーション

別の場所で扱う内容:

  • 言語に依存しない契約ドキュメントを必要とする、サードパーティー向けまたは TypeScript 以外のコンシューマー向け API
  • キャッシュポリシー、リトライ、無効化のセマンティクス。このスキルでは、プロシージャをクエリークライアントが利用するオプションに変換する方法を定め、クライアント側でそのオプションの処理方法を決めます
  • .input() で使うスキーマライブラリ。利用中のバージョンがサポートするバリデーターならどれでも使えます。例ではそのうち1つを示します
  • セッションとトークンの発行。コンテキストはセッションを利用しますが、セッション自体は確立しません

哲学

API の記述はどこにもありません。スキーマファイルも、生成されたクライアントも、両者の間に必要なビルド工程もありません。ルーターから推論される型そのものが契約であり、コードベース全体で型をインポートして共有します。

これにより、すぐに正確さが得られます。プロシージャの戻り値の型を変更すると、再生成の手順を忘れる心配もなく、同じ型チェックでそのプロシージャを呼び出すすべての箇所にエラーが表示されます。その代わり、複数言語のサポート、公開された契約、HTTP キャッシュを諦めることになります。呼び出しはデフォルトで POST になるため、エンドポイントの前段にある CDN にはキャッシュするものがありません。そのため、tRPC は1つの TypeScript コードベース内の内部 API には適していますが、公開 API にはあまり適していません。


基本パターン

パターン 1:初期化

アプリケーションごとに1回だけ初期化し、ルーターの組み立てに使うファクトリーをエクスポートします。

const t = initTRPC.context<Context>().create({
  transformer: superjson, // Date, Map and Set survive the wire
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;

エラーフォーマッターを使うと、バリデーションエラーを1つのメッセージではなく、フォームでフィールドごとに表示できる形式に変換できます。

完全なコード:examples/core.md — リクエストごとのコンテキストファクトリーを含みます


パターン 2:プロシージャと入力のバリデーション
const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

export const userRouter = router({
  create: protectedProcedure
    .input(createUserSchema)
    .mutation(async ({ input, ctx }) => {
      // input: { email: string; name: string } — validated, and typed from the same schema
      return ctx.db.user.create({ data: input });
    }),
});

完全なコード:examples/core.md


パターン 3:コンテキストの型を絞り込むミドルウェア
const isAuthenticated = middleware(async ({ ctx, next }) => {
  if (!ctx.session || !ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
  return next({ ctx: { ...ctx, session: ctx.session, user: ctx.user } });
});

export const protectedProcedure = publicProcedure.use(isAuthenticated);

next({ ctx }) の戻り値の型によって、protectedProcedure を使って構築したすべてのプロシージャで ctx.user が null にならないことが保証されます。これにより、チェックを忘れると、レビュー時の指摘ではなくコンパイルエラーになります。

完全なコード:examples/middleware.md


パターン 4:型の橋渡し
export const appRouter = router({ user: userRouter, post: postRouter });
export type AppRouter = typeof appRouter;

type RouterInputs = inferRouterInputs<AppRouter>;
type RouterOutputs = inferRouterOutputs<AppRouter>;

type User = RouterOutputs["user"]["getById"];

コンポーネントの props では手書きのインターフェースではなく RouterOutputs[...] を使うため、プロシージャからフィールドを削除すると、そのフィールドを読み取るすべてのコンポーネントにエラーが表示されます。

完全なコード:examples/core.md


### パターン 5:型付きクエリおよびミューテーションのオプション
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();

const trpc = useTRPC();
const { data } = useQuery(trpc.user.getById.queryOptions({ id: userId }));

各プロシージャには queryOptions()、mutationOptions()、infiniteQueryOptions()、queryKey() が用意されています。オプションオブジェクトは通常のフックに渡されるため、クエリクライアントでオプションオブジェクトに対して行えることは、ここでもすべて利用できます。

コード全文:examples/core.md — プロバイダー、リンク、コンポーネント


パターン 6:エラー
// Server
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
throw new TRPCError({
  code: "INTERNAL_SERVER_ERROR",
  message: "Failed to delete",
  cause: error,
});
// Client
onError: (error) => {
  switch (error.data?.code) {
    case "NOT_FOUND":
      return toast.error("Not found");
    case "FORBIDDEN":
      return toast.error("Not allowed");
  }
};

cause はサーバーログ用に元のスタックを保持しつつ、クライアントにはコードとメッセージだけを返します。コードとステータスの対応表は reference.md にあります。


パターン 7:楽観的更新

キャンセル、スナップショット、書き込み、失敗時のロールバック、完了時の無効化――この5つすべてが必要です。ロールバックがあることで、楽観的な書き込みが安全になります。

const toggleTodo = useMutation({
  ...trpc.todo.toggle.mutationOptions(),
  onMutate: async ({ id }) => {
    await queryClient.cancelQueries({ queryKey: trpc.todo.list.queryKey() });
    const previousTodos = queryClient.getQueryData(trpc.todo.list.queryKey());
    queryClient.setQueryData(trpc.todo.list.queryKey(), (old) =>
      toggle(old, id),
    );
    return { previousTodos };
  },
  onError: (err, vars, context) =>
    queryClient.setQueryData(trpc.todo.list.queryKey(), context?.previousTodos),
  onSettled: () =>
    queryClient.invalidateQueries({ queryKey: trpc.todo.list.queryKey() }),
});

cancelQueries は省略できません。処理中の再フェッチが楽観的な書き込みの後に完了すると、変更前のサーバー状態で上書きされます。

コード全文:examples/optimistic-updates.md


<red_flags>

注意事項

実行時に問題が発生します:

  • export type AppRouter がない場合、クライアントは unknown を推論し、すべてのプロシージャ呼び出しが型なしになります。これを省略したファイルでエラーになることもありません。
  • v11 で createTRPCClient() にトランスフォーマーを設定すると、クライアントの作成時に例外が発生し、本来設定すべきリンクが示されます。
  • 片方だけにトランスフォーマーを設定すると、両端でワイヤ形式の認識が食い違い、Date、Map、Set が別の形式で届きます。
  • サブスクリプションで observable() を使うと、v10 の形式になります。v11 のサブスクリプションは非同期ジェネレーターです。
  • ミドルウェアで rawInput を使うと、v11 では await getRawInput() に置き換えられているため動作しません。
  • スナップショットと onError のない楽観的な書き込みでは、ミューテーションが失敗しても、実際には起きていない変更が UI に表示されたままになります。

予期しない動作:

  • httpBatchLink は同時に行われた呼び出しを1つのリクエストにまとめるため、バッチ内のすべてのプロシージャで HTTP ステータスが共有されます。1つで 401 が発生すると、ほかの呼び出しも同じステータスを受け取ります。
  • コンテキストはリクエストごとに構築されるため、そこに保存されたミュータブルな値は破棄され、共有されません。
  • ミドルウェアは宣言順に実行されるため、各レイヤーから参照できる情報が変わります。レート制限の前に認証を行うと、既知のユーザーをキーにして制限できます。
  • ミューテーションを再試行すると書き込みが繰り返されます。ミューテーションでは retry: false を設定してください。
  • 再開可能なサブスクリプションでは、再開位置を示す tracked() の情報を格納できるよう、入力スキーマに lastEventId が必要です。
  • プロシージャ本体に認証チェックを書くと、型システム上 ctx.user は null 許容のままです。そのため、チェックを忘れた次のプロシージャもコンパイルできます。

</red_flags>

コメント