intermediate
tRPC API boundaries
Use tRPC for TypeScript monorepo productivity while respecting runtime validation, auth, deployment, and public-contract limits.
tRPC shares TypeScript types between client and server through routers and procedures — excellent **monorepo** productivity without manual OpenAPI sync.
const appRouter = router({
order: router({
byId: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(({ input, ctx }) => ctx.orders.getById(input.id)),
create: protectedProcedure
.input(createOrderSchema)
.mutation(({ input, ctx }) => ctx.orders.create(input)),
}),
});
export type AppRouter = typeof appRouter;
Boundaries to respect:
- **Runtime validation** (Zod) even with static types — external input is untrusted.
- **Auth middleware** on procedures (`protectedProcedure`), not ad-hoc checks in handlers.
- **Public vs internal**: tRPC shines for first-party apps; third-party public APIs usually need OpenAPI/REST for language-agnostic contracts.
- **Deployment**: separate serverless functions may need HTTP batching config; WebSockets for subscriptions are optional and infra-specific.
On interviews: when tRPC is appropriate (TS full-stack team), how you would expose a stable external API alongside tRPC, and validation at the boundary.
Common pitfalls: trusting compile-time types at runtime, leaking `AppRouter` types to untrusted consumers, god routers, and no error shape discipline.
The trade-off is end-to-end TS speed versus ecosystem openness — document escape hatches for non-TS clients.
Checklist:
- Zod (or similar) on every procedure input.
- Auth as composable middleware.
- Split public BFF from internal routers if needed.
- Consistent error mapping for the client.