| name | trpc-router |
| description | Add or modify tRPC routes with type-safe procedures and authentication. Use when creating new API endpoints that need type safety or updating existing tRPC routes. |
| allowed-tools | Read, Edit, Write, Grep, Glob |
tRPC Router Skill
This skill helps you work with tRPC routes in apps/api/src/trpc/.
When to Use This Skill
- Creating new type-safe API endpoints
- Adding authentication/authorization to routes
- Implementing input validation with Zod
- Creating public vs. protected procedures
- Debugging tRPC errors or type issues
tRPC Architecture
tRPC provides end-to-end type safety between API and client:
apps/api/src/trpc/
├── root.ts # Root router combining all routers
├── trpc.ts # tRPC context and procedure definitions
└── routers/ # Individual feature routers
├── cars.ts
├── coe.ts
└── posts.ts
Key Patterns
1. Creating a New Router
import { z } from "zod";
import { createTRPCRouter, publicProcedure, protectedProcedure } from "../trpc";
export const myRouter = createTRPCRouter({
// Public endpoint (no auth required)
getAll: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
offset: z.number().min(0).default(0),
}))
.query(async ({ input, ctx }) => {
const { limit, offset } = input;
// Query database using ctx.db
return await ctx.db.query.myTable.findMany({
limit,
offset,
});
}),
// Protected endpoint (requires authentication)
create: protectedProcedure
.input(z.object({
name: z.string().min(1),
description: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
// ctx.user is available in protected procedures
return await ctx.db.insert(myTable).values({
...input,
userId: ctx.user.id,
});
}),
});
2. Input Validation with Zod
Always validate inputs using Zod schemas:
import { z } from "zod";
const carInputSchema = z.object({
make: z.string().min(1).max(100),
model: z.string().min(1).max(100),
year: z.number().int().min(1900).max(new Date().getFullYear() + 1),
price: z.number().positive().optional(),
});
export const carsRouter = createTRPCRouter({
create: protectedProcedure
.input(carInputSchema)
.mutation(async ({ input, ctx }) => {
// Input is fully typed and validated
return await ctx.db.insert(cars).values(input);
}),
});
3. Procedure Types
Query - For reading data (GET-like):
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
// Return data
})
Mutation - For writing data (POST/PUT/DELETE-like):
update: protectedProcedure
.input(z.object({ id: z.string(), name: z.string() }))
.mutation(async ({ input }) => {
// Modify data
})
Subscription - For real-time updates (WebSocket):
onUpdate: publicProcedure
.subscription(() => {
// Stream updates
})
Common Tasks
Adding a New Router
- Create router file in
apps/api/src/trpc/routers/ - Define procedures with input validation
- Import and add to root router in
apps/api/src/trpc/root.ts:import { myRouter } from "./routers/my-router"; export const appRouter = createTRPCRouter({ cars: carsRouter, coe: coeRouter, my: myRouter, // Add here }); - Types are automatically available in client apps
Adding Authentication
Use protectedProcedure for authenticated routes:
import { protectedProcedure } from "../trpc";
export const adminRouter = createTRPCRouter({
dangerousOperation: protectedProcedure
.mutation(async ({ ctx }) => {
// ctx.user is guaranteed to exist
console.log("User:", ctx.user.email);
// Perform operation
}),
});
Error Handling
Throw tRPC errors with appropriate codes:
import { TRPCError } from "@trpc/server";
export const carsRouter = createTRPCRouter({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
const car = await ctx.db.query.cars.findFirst({
where: eq(cars.id, input.id),
});
if (!car) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Car with id ${input.id} not found`,
});
}
return car;
}),
});
Error codes:
BAD_REQUEST- Invalid inputUNAUTHORIZED- Not authenticatedFORBIDDEN- Not authorizedNOT_FOUND- Resource not foundINTERNAL_SERVER_ERROR- Server error
Context Access
The ctx parameter provides access to:
ctx.db- Drizzle database instancectx.redis- Redis clientctx.user- Current user (in protectedProcedure)ctx.req- HTTP request objectctx.res- HTTP response object
Type Safety
tRPC automatically generates TypeScript types:
// In client code (e.g., Next.js app)
import { trpc } from "@/lib/trpc";
// Fully typed, autocomplete works!
const { data, isLoading } = trpc.cars.getAll.useQuery({
limit: 10,
offset: 0,
});
// Type error if input is wrong
const { mutate } = trpc.cars.create.useMutation();
mutate({
make: "Toyota",
// Error: missing required field 'model'
});
Testing tRPC Routes
Create tests in apps/api/src/trpc/routers/__tests__/:
import { describe, it, expect } from "vitest";
import { createCaller } from "../../trpc";
import { appRouter } from "../../root";
describe("cars router", () => {
it("should get all cars", async () => {
const caller = createCaller(appRouter);
const result = await caller.cars.getAll({ limit: 10, offset: 0 });
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
});
});
Run tests:
pnpm -F @sgcarstrends/api test -- src/trpc
References
- tRPC Documentation: Use Context7 to get latest tRPC docs
- Related files:
apps/api/src/trpc/trpc.ts- Procedure definitionsapps/api/src/trpc/root.ts- Root routerapps/api/CLAUDE.md- API service documentation
Best Practices
- Validation: Always use Zod for input validation
- Type Safety: Let TypeScript infer types, avoid manual typing
- Error Handling: Use appropriate tRPC error codes
- Security: Use
protectedProcedurefor sensitive operations - Naming: Use clear, RESTful names (get, getById, create, update, delete)
- Testing: Write tests for all procedures
- Documentation: Add JSDoc comments for complex logic