trpc

tRPC

End-to-end typesafe APIs for TypeScript monorepos without code generation

Details

Language / Topic
typescriptTypeScript
Category
framework

Rules

balanced
- Define routers with `router({ hello: publicProcedure.input(z.string()).query(({ input }) => `Hello ${input}`) })` — use `.query()` for reads and `.mutation()` for writes.
- Validate all inputs with Zod: `publicProcedure.input(z.object({ id: z.string().uuid() }))` — tRPC infers the TypeScript type automatically.
- Create a shared `AppRouter` type export from the server and import it in the client: `createTRPCReact<AppRouter>()` for full e2e type safety.
- Use `middleware` for cross-cutting concerns like auth: `const protectedProcedure = publicProcedure.use(authMiddleware)`.
- Keep routers modular — one file per domain (e.g. `routers/users.ts`, `routers/posts.ts`) merged via `mergeRouters`.
- Define procedures with `publicProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => ctx.db.user.findUnique({ where: { id: input.id } }))` — `.query()` for GET-like reads, `.mutation()` for writes.
- Validate every input with Zod and let tRPC infer the output type — avoid manually annotating return types unless enforcing a specific output shape.
- Export `AppRouter` type from the server entry point and import it into the React client: `const trpc = createTRPCReact<AppRouter>()` — this is what gives end-to-end type safety with zero code generation.
- Compose reusable middleware for authentication: `const authedProcedure = t.procedure.use(async ({ ctx, next }) => { if (!ctx.session) throw new TRPCError({ code: 'UNAUTHORIZED' }); return next({ ctx: { user: ctx.session.user } }) })`.
- Organize routers by domain (`users.ts`, `posts.ts`) and merge with `router({ users: usersRouter, posts: postsRouter })` in a root `_app.ts`.
- Use `TRPCError` with appropriate codes (`NOT_FOUND`, `BAD_REQUEST`, `FORBIDDEN`) — the client receives the structured error with code and message.