trpc-router について
サーバーコードで tRPC ルーターを実装するためのガイドです。プロシージャ構成、中間ウェアによるモデル注入、入力検証、エラー処理、エンドポイントの規約を説明します。
主な機能
- 01中間ウェアによるモデル注入
- 02Zod による入力検証
- 03クエリとミューテーションのパターン
- 04集約詳細エンドポイント
- 05ルーター命名とログの規約
活用シーン
- 01ルータープロシージャの追加
- 02関連データを単一エンドポイントで取得
- 03ルーターのエラー処理の統一
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ lobehub/ lobehub --skill trpc-routerインストール後、Claude Code で/trpc-routerを入力して読み込まれたか確認します。
詳細
TRPC ルーターガイド
ファイルの場所
- ルーター:
apps/server/src/routers/lambda/<domain>.ts - ヘルパー:
apps/server/src/routers/lambda/_helpers/ - スキーマ:
apps/server/src/routers/lambda/_schema/
ルーターの構成
インポート
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { SomeModel } from '@/database/models/some';
import { authedProcedure, router } from '@/libs/trpc/lambda';
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
ミドルウェア:モデルを ctx に注入する
各プロシージャ内で new Model(ctx.serverDB, ctx.userId) を作成するのではなく、必ずミドルウェアを使ってモデルを ctx に注入してください。
const domainProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
const { ctx } = opts;
return opts.next({
ctx: {
fooModel: new FooModel(ctx.serverDB, ctx.userId),
barModel: new BarModel(ctx.serverDB, ctx.userId),
},
});
});
次に、プロシージャ内で ctx.fooModel を使用します。
// Good
const model = ctx.fooModel;
// Bad - don't create models inside procedures
const model = new FooModel(ctx.serverDB, ctx.userId);
例外:モデルに別の userId が必要な場合(例:ウォッチドッグで複数のユーザーのタスクを反復処理する場合)は、インラインで作成します。
プロシージャのパターン
export const fooRouter = router({
// Query
find: domainProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
try {
const item = await ctx.fooModel.findById(input.id);
if (!item) throw new TRPCError({ code: 'NOT_FOUND', message: 'Not found' });
return { data: item, success: true };
} catch (error) {
if (error instanceof TRPCError) throw error;
console.error('[foo:find]', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to find item',
});
}
}),
// Mutation
create: domainProcedure.input(createSchema).mutation(async ({ input, ctx }) => {
try {
const item = await ctx.fooModel.create(input);
return { data: item, message: 'Created', success: true };
} catch (error) {
if (error instanceof TRPCError) throw error;
console.error('[foo:create]', error);
throw new TRPCError({
cause: error,
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to create',
});
}
}),
});
集約詳細エンドポイント
複数の関連データが必要なビューでは、すべてを並列に取得する単一の detail プロシージャを作成します。
detail: domainProcedure.input(idInput).query(async ({ input, ctx }) => {
const item = await resolveOrThrow(ctx.fooModel, input.id);
const [children, related] = await Promise.all([
ctx.fooModel.findChildren(item.id),
ctx.barModel.findByFooId(item.id),
]);
return {
data: { ...item, children, related },
success: true,
};
}),
これにより、CLI やフロントエンドが N 回の逐次リクエストを行う必要がなくなります。
規約
- 戻り値の形:クエリは
{ data, success: true }、ミューテーションは{ data?, message, success: true } - エラー処理:
TRPCErrorは再スローし、それ以外はconsole.errorと新しいTRPCErrorでラップします - 入力検証:
zodスキーマを使用し、ファイルの先頭で定義します - ルーター名:
export const fooRouter = router({ ... }) - プロシージャ名:ルーターオブジェクト内でアルファベット順にします
- ログのプレフィックス:
[domain:procedure]形式(例:[task:create])
コメント
Markdown 対応