Web Development

Trying tRPC instead of GraphQL for internal, single-consumer services

DEV

GraphQL earns its complexity when multiple, differently-shaped consumers need the same data through a single flexible query interface, a public API serving both a web client and a mobile app with genuinely different data needs is the textbook case where that flexibility pays for itself. For internal tools with exactly one frontend consumer, that flexibility is often pure overhead, a schema layer, resolver logic, and query flexibility that nobody on the single consuming team actually needs, so we tried tRPC on a few of them instead this year.

End-to-end type safety without maintaining a separate schema layer removed real boilerplate for these single-consumer cases. With GraphQL, even a small internal tool ends up needing a schema definition, resolvers that map that schema onto actual data access, and a client-side code generation step to get type safety on the frontend, three distinct layers of work for a service that has exactly one consumer who could, in principle, just call a typed function directly. tRPC's approach collapses that down to defining a typed procedure on the server and calling it directly from the client with full type inference, no schema definition step and no code generation step standing between writing the server logic and consuming it with type safety on the frontend.

The developer experience difference was most obvious during active development, when an API shape is still evolving. Changing a GraphQL schema field's type meant updating the schema, regenerating client types, and fixing whatever broke, three separate steps even for a small change. The equivalent tRPC change is just changing the TypeScript type on the server, with the client immediately seeing a type error anywhere it needs updating, no regeneration step required. For internal tools that iterate quickly and don't need to serve external consumers with API stability guarantees, that faster loop mattered more day to day than any theoretical advantage GraphQL's flexibility offered.

We haven't replaced GraphQL anywhere it's genuinely serving multiple, differently-shaped consumers, and we don't think teams should treat this as a wholesale replacement recommendation. A couple of our client-facing APIs continue to use GraphQL specifically because they're consumed by both a web frontend and a mobile app with real differences in what data each one needs per screen, exactly the scenario GraphQL's field-selection flexibility was designed to solve well. Forcing those into a tRPC-style single-consumer model would mean giving up real value GraphQL provides there.

tRPC has become our default for anything simpler: internal admin tools, single-team dashboards, and services where the frontend and backend are maintained by the same small team and evolve together. The decision rule we've settled into is straightforward, count the genuinely distinct consumers a service needs to support with meaningfully different data shapes; more than one strongly favors GraphQL or a similarly flexible API layer, exactly one favors tRPC's simpler, more directly typed approach. It's a smaller decision than either technology's advocates sometimes make it sound, but getting it right per project has removed a meaningful amount of unnecessary boilerplate across our internal tooling this year.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients