tRPC Changed How I Think About APIs
2026-01-15
I used to write API types by hand. Define the response shape in the backend, copy it to the frontend, keep them in sync manually, watch them drift apart over months. It was fine until it wasn't — a renamed field, a new nullable column, a changed enum value. Runtime errors in production because the types lied.
tRPC eliminated an entire category of bugs from my work. The setup in Materia: Drizzle schema defines the database tables in TypeScript. tRPC routers import those types and define procedures with Zod input validation. React Query hooks are generated from the router types. Change a column name in the schema and TypeScript tells you every component that breaks, all the way down to the button text.
The flow looks like this. A procedure in packages/trpc defines its input with Zod and returns data queried through Drizzle. The return type is inferred — I never write it. On the client, trpc.products.get.useQuery({ slug }) gives me full autocompletion on the response. Hover over any field and you see the type that originated in the database schema. One source of truth, zero manual type definitions.
Materia has over twenty tRPC routers — products, projects, notes, search, imports, recommendations, saves. Each one composes services from the context: ctx.notes.create(), ctx.search.query(). The context itself is typed, with protected procedures that narrow ctx.session and ctx.user to non-null. Rate limiting is a middleware layer with named tiers — colorSearch at 120 requests per minute, ai at 10.
The part that changed my thinking was the refactoring confidence. Materia's product schema has JSONB attributes, vector embeddings, percentile rankings, hierarchical categories. Complex stuff. When I restructure any of it, the type system catches every downstream consumer instantly. No grep, no hoping, no runtime surprises. The compiler is the integration test.
I use superjson as the transformer, which means Dates, Maps, and Sets survive the serialisation boundary. Custom fetch wrapper with a 90-second timeout because some operations — image generation, bulk imports — take a while. These are the boring infrastructure decisions that make the whole system work.
I don't think I can go back to writing REST APIs with manual type definitions. The cognitive overhead of keeping types in sync across a boundary was significant, and I didn't fully appreciate it until it was gone.