Rimo's dashboard originally used tRPC for all client-server communication. tRPC was a natural early choice: single TypeScript monorepo, no codegen step, full type inference from server router straight into the React client. It worked well while Rimo had one consumer (the internal dashboard) and a small API surface.
As the product grew, three pressures built up:
GraphQL (via Apollo Server/Client) addressed all three: a language-agnostic schema, field-level selection per client, and introspection as free documentation.
Before (tRPC)
React client → tRPC client (typed proxy) → tRPC router → procedure handlers → Postgres (Prisma)
Type safety came entirely from TypeScript inference — the client imported the server's AppRouter type directly. Zero codegen, but zero portability outside the monorepo.
After (GraphQL)
React client → Apollo Client (normalized cache) → Apollo Server → resolvers → DataLoader batch layer → Postgres (Prisma)
Type safety now comes from a schema-first contract: the SDL schema is the source of truth, and GraphQL Code Generator produces TypeScript types for both resolvers (server-side) and typed hooks (client-side).
graphql-code-generator produces types for any consumer — including future non-TS clients — not just ones inside the monorepo.zod as the runtime validation layer at the resolver boundary (input validation, same library we'd used with tRPC), so business-logic validation didn't change — only the transport and type-generation layer did. This is a good detail to mention because it shows the migration wasn't a full rewrite of validation logic, just the layer around it.