After maintaining a REST API with 47 endpoints and a growing TypeScript frontend, I was spending more time keeping types synchronized than building features. That's when I decided to migrate to tRPC.
The Problem with REST + TypeScript
Our REST API had manually maintained type definitions on both the client and server. Every time we added a field to a response, we had to update:
- The database schema
- The server-side response type
- The API documentation
- The client-side type definition
This four-step dance was error-prone. Types would drift apart, leading to runtime errors that TypeScript was supposed to prevent. We'd ship a feature, only to discover in production that the frontend expected a string where the backend now returned an object.
Enter tRPC
tRPC eliminates the API boundary entirely. Your server-side router definition IS the client-side type. Change a return type on the server, and your IDE immediately shows you every client-side usage that needs updating.
Here's how a basic tRPC router looks:
And on the client, you get full type inference:
The migration was surgical — we replaced endpoints one at a time, running tRPC alongside our existing REST API. We completed the migration of 47 endpoints in about three weeks.
The DX Improvement
The developer experience improvement was immediate and dramatic. Auto-complete now works across the full stack. Hover over a useQuery call and you see the exact return type, inferred all the way from the database query. Refactoring became fearless — rename a field on the server, and TypeScript catches every client reference instantly.
The Trade-offs
What We Gained
- Zero type drift — impossible for client and server types to disagree
- Autocomplete everywhere — the client knows every available procedure and its exact input/output shape
- Faster iteration — removing the type synchronization step saved roughly 30% of feature development time
- Smaller bundle — no need for axios or fetch wrappers; tRPC's client is lightweight
What We Lost
- HTTP caching — tRPC uses POST for mutations and batched queries, complicating CDN caching
- API discoverability — no more Swagger/OpenAPI docs for external consumers
- Ecosystem lock-in — tRPC is TypeScript-only; if you need a mobile client in Swift or Kotlin, you'll need a separate API
- Debugging — REST endpoints are easy to test with curl; tRPC procedures require the client
Would I Do It Again?
Absolutely — for internal, TypeScript-only applications. For public APIs or multi-language consumers, REST with generated types (like OpenAPI + code generation) remains the better choice.
The key insight isn't that tRPC is better than REST.
It's that the right abstraction depends on your consumer. Internal app with a TypeScript frontend? tRPC.
Public API with diverse clients? REST with OpenAPI. The mistake is treating this as a religious debate rather than an engineering decision with clear trade-offs.
Thank you for reading 💖
