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.