web-data-fetching-trpc について
TypeScript 開発者向けに、ルーター、プロシージャ、入力検証、クライアント側の型推論など、tRPC のパターンを解説するスキルです。v11 の連携方法やエラー処理、ミドルウェア、サブスクリプションも扱います。
主な機能
- 01ルーター型の共有
- 02プロシージャ入力の検証
- 03ミドルウェアによるコンテキスト型の絞り込み
- 04型付きクエリとミューテーションのオプション
- 05エラー処理とサブスクリプション
活用シーン
- 01エンドツーエンドで型安全な内部 API の構築
- 02プロシージャ入力への実行時検証の追加
- 03認証済みプロシージャのコンテキスト定義
- 04ページネーション、サブスクリプション、楽観的更新の実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-data-fetching-trpcインストール後、Claude Code で/web-data-fetching-trpcを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/core.mdfile-uploads.mdinfinite-queries.mdmiddleware.mdoptimistic-updates.mdsubscriptions.md詳細
tRPC のパターン
クイックガイド: tRPC は、生成されたスキーマではなく、エクスポートされた 1 つの型を通じてサーバーからクライアントへ型を引き継ぐため、
export type AppRouter = typeof appRouterがその橋渡しのすべてです。これがなければ、その後の処理はすべて機能しません。プロシージャは検証済みの入力を受け取り、値を返します。TRPCErrorのコードは HTTP ステータスになります。ミドルウェアはコンテキストの型を絞り込むため、認証済みプロシージャのctx.userは null になりません。v11 ではトランスフォーマーがリンク内に移動し、サブスクリプションは非同期ジェネレーターになりました。また、現在の React 統合には@trpc/tanstack-react-queryを使用します。
詳細なリソース:
- examples/core.md — 初期化、コンテキスト、CRUD ルーター、プロバイダー、推論される型、
queryOptions - examples/middleware.md — ロギング、レート制限、リソース単位のアクセス制御
- examples/infinite-queries.md — カーソルページネーションの全手順
- examples/optimistic-updates.md — スナップショットの取得とロールバックの一連の処理
- examples/subscriptions.md — 再開可能なイベント ID を使った非同期ジェネレーターのサブスクリプション
- examples/file-uploads.md — 入力スキーマ内の
File(v11 以降) - reference.md — エラーコードと HTTP ステータスの対応表、バッチ処理と無効化に関する注意事項、v10 → v11 の移行
該当する方法
@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>