Skip to main content

trpc-router

Use for server TRPC routers, procedures and API endpoints in apps/server/src/routers.

Jump to install

Source facts

Repository
lobehub/lobehub
Last source activity
September 7, 2026 at 11:06
Detected SKILL.md language
English
Stars
82,793
Forks
15,917

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
trpc-router
description
Use for server TRPC routers, procedures and API endpoints in apps/server/src/routers.
user-invocable
false
# TRPC Router Guide ## File Location - Routers: `apps/server/src/routers/lambda/<domain>.ts` - Helpers: `apps/server/src/routers/lambda/_helpers/` - Schemas: `apps/server/src/routers/lambda/_schema/` ## Router Structure ### Imports ```typescript 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'; ``` ### Middleware: Inject Models into ctx **Always use middleware to inject models into `ctx`** instead of creating `new Model(ctx.serverDB, ctx.userId)` inside every procedure. ```typescript 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), }, }); }); ``` Then use `ctx.fooModel` in procedures: ```typescript // Good const model = ctx.fooModel; // Bad - don't create models inside procedures const model = new FooModel(ctx.serverDB, ctx.userId); ``` **Exception**: When a model needs a different `userId` (e.g., watchdog iterating over multiple users' tasks), create it inline. ### Procedure Pattern ```typescript 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', }); } }), }); ``` ### Aggregated Detail Endpoint For views that need multiple related data, create a single `detail` procedure that fetches everything in parallel: ```typescript 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, }; }), ``` This avoids the CLI or frontend making N sequential requests. ## Conventions - Return shape: `{ data, success: true }` for queries, `{ data?, message, success: true }` for mutations - Error handling: re-throw `TRPCError`, wrap others with `console.error` + new `TRPCError` - Input validation: use `zod` schemas, define at file top - Router name: `export const fooRouter = router({ ... })` - Procedure names: alphabetical order within the router object - Log prefix: `[domain:procedure]` format, e.g. `[task:create]`
View on GitHub