Add Router Skill
Create a new tRPC router with CRUD procedures, OpenAPI meta, and test file for x4-mono.
Arguments
The user describes the resource. If unclear, ask for:
- Resource name (singular, camelCase for router, e.g.,
comment) - Which procedures: list, get, create, update, delete (default: all)
- Auth level: public, protected, or admin
- Whether it needs ownership checks
File Locations
- Router:
apps/api/src/routers/{name}.ts - Registration:
apps/api/src/routers/index.ts - Tests:
apps/api/src/__tests__/{name}.test.ts - Schemas:
packages/shared/utils/validators.ts(if not already there)
Router Template
Reference: apps/api/src/routers/projects.ts
typescript1import { eq, sql } from 'drizzle-orm'; 2import { router, publicProcedure, protectedProcedure } from '../trpc'; 3import { Errors } from '../lib/errors'; 4import { tableName } from '@x4/database'; 5import { 6 CreateEntitySchema, 7 UpdateEntitySchema, 8 EntityResponseSchema, 9 EntityListResponseSchema, 10 IdParamSchema, 11 PaginationSchema, 12} from '@x4/shared/utils'; 13 14export const entityRouter = router({ 15 list: publicProcedure 16 .meta({ openapi: { method: 'GET', path: '/entities', tags: ['Entity'] } }) 17 .input(PaginationSchema) 18 .output(EntityListResponseSchema) 19 .query(async ({ ctx, input }) => { 20 const items = await ctx.db.select().from(tableName).limit(input.limit).offset(input.offset); 21 return { items, total: items.length, limit: input.limit, offset: input.offset }; 22 }), 23 24 get: publicProcedure 25 .meta({ openapi: { method: 'GET', path: '/entities/{id}', tags: ['Entity'] } }) 26 .input(IdParamSchema) 27 .output(EntityResponseSchema) 28 .query(async ({ ctx, input }) => { 29 const [record] = await ctx.db.select().from(tableName).where(eq(tableName.id, input.id)); 30 if (!record) throw Errors.notFound('Entity').toTRPCError(); 31 return record; 32 }), 33 34 create: protectedProcedure 35 .meta({ openapi: { method: 'POST', path: '/entities', tags: ['Entity'], protect: true } }) 36 .input(CreateEntitySchema) 37 .output(EntityResponseSchema) 38 .mutation(async ({ ctx, input }) => { 39 const [record] = await ctx.db 40 .insert(tableName) 41 .values({ ...input, ownerId: ctx.user.userId }) 42 .returning(); 43 return record; 44 }), 45 46 update: protectedProcedure 47 .meta({ openapi: { method: 'PATCH', path: '/entities/{id}', tags: ['Entity'], protect: true } }) 48 .input(UpdateEntitySchema) 49 .output(EntityResponseSchema) 50 .mutation(async ({ ctx, input }) => { 51 const { id, ...data } = input; 52 const [existing] = await ctx.db.select().from(tableName).where(eq(tableName.id, id)); 53 if (!existing) throw Errors.notFound('Entity').toTRPCError(); 54 if (existing.ownerId !== ctx.user.userId && ctx.user.role !== 'admin') { 55 throw Errors.forbidden('Not the owner').toTRPCError(); 56 } 57 const [updated] = await ctx.db 58 .update(tableName) 59 .set(data) 60 .where(eq(tableName.id, id)) 61 .returning(); 62 return updated; 63 }), 64 65 delete: protectedProcedure 66 .meta({ 67 openapi: { method: 'DELETE', path: '/entities/{id}', tags: ['Entity'], protect: true }, 68 }) 69 .input(IdParamSchema) 70 .output(z.object({ success: z.boolean() })) 71 .mutation(async ({ ctx, input }) => { 72 const [existing] = await ctx.db.select().from(tableName).where(eq(tableName.id, input.id)); 73 if (!existing) throw Errors.notFound('Entity').toTRPCError(); 74 if (existing.ownerId !== ctx.user.userId && ctx.user.role !== 'admin') { 75 throw Errors.forbidden('Not the owner').toTRPCError(); 76 } 77 await ctx.db.delete(tableName).where(eq(tableName.id, input.id)); 78 return { success: true }; 79 }), 80});
Registration
Add to apps/api/src/routers/index.ts:
typescript1import { entityRouter } from './entity'; 2 3export const appRouter = router({ 4 // ... existing routers 5 entity: entityRouter, 6});
Workflow
- Ensure Zod schemas exist (use
/add-schemafirst if needed) - Ensure database table exists (use
/add-tablefirst if needed) - Create router file at
apps/api/src/routers/{name}.ts - Register in
apps/api/src/routers/index.ts - Run
bun turbo type-checkto verify AppRouter updates - Create test file at
apps/api/src/__tests__/{name}.test.ts(use/add-testor bun-test-gen) - Run
bun test --cwd apps/apito verify