Context

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:

  1. Multiple consumers. A partner-facing embed and an internal admin tool both needed to hit the same data, but tRPC's type inference only works when client and server share a TS toolchain — it breaks down for external or non-TS consumers.
  2. Over-fetching on dashboard views. Several dashboard widgets called the same tRPC procedures but only needed 2-3 fields out of a much larger payload. Each widget was either duplicating narrow endpoints or pulling the full object.
  3. No self-describing contract. New engineers had to read resolver implementations to know what was available. There was no introspectable schema, so the API's shape lived only in code.

GraphQL (via Apollo Server/Client) addressed all three: a language-agnostic schema, field-level selection per client, and introspection as free documentation.


Architecture: before and after

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).


Type safety: what we gained and what we gave up