tRPC Made Me Forget REST Exists

REST isn’t “dead,” but after using tRPC in a TypeScript-heavy codebase, it’s hard to unsee how much ceremony you inherit—versioning chaos, schema drift, and that nagging question of whether the backend and frontend still agree.
tRPC doesn’t just make API calls feel nicer. It removes the API contract problem at the source: the same TypeScript types that describe your server procedure become available to your client automatically—no OpenAPI spec, no code generation, and far fewer “surprise” mismatches.
The real problem isn’t HTTP—it’s the contract⌗
REST is fundamentally an HTTP convention. The contract—the part everyone actually cares about—is usually captured somewhere else: an OpenAPI document, a hand-written JSON schema, a shared type package, or the worst option of all: documentation that developers interpret differently.
That’s where problems breed:
- Input shape drift: frontend sends
{ userId: "123" }, backend expects{ id: number }. - Output shape drift: frontend reads
data.items, backend returnsdata.results. - Version mismatches: you deploy the backend, the frontend lags, and nobody realizes the procedure signature changed.
- Maintenance overhead: keeping schemas, generators, and types in sync becomes a job of its own.
You can paper over this with discipline, but discipline doesn’t fix structural risk. You can be the best engineer on the team and still get burned when the system encourages divergence.
What tRPC changes: one source of truth for both sides⌗
With tRPC, you define procedures on the server in TypeScript. The client consumes them via a type-safe API that’s inferred from the server definition.
The important part: the contract is not “described” in a separate artifact. It is the code itself.
Here’s the basic shape of the mental model:
- Server: you declare a procedure with input validation and an output type.
- Client: you call that procedure and TypeScript already knows the input/output types.
- Sync by construction: change the server procedure, and the client immediately reflects the new types (or fails to compile).
No OpenAPI spec. No generator pipeline. No “run codegen, commit output, pray.” If you’re using a TypeScript monorepo, it gets even cleaner because the server router and client can share types directly.
A concrete example: “getUser” that can’t drift⌗
Suppose your server has a procedure:
- input:
{ userId: string } - output:
{ id: string; name: string; role: "admin" | "user" }
With tRPC, when you call it from the client:
- TypeScript will enforce the input shape.
- TypeScript will tell you exactly what fields exist in the result.
- If you rename
userIdtoid, or add/remove a field, the client compile fails right away.
That’s not “nice autocomplete.” That’s eliminating a whole class of integration bugs before runtime even has a chance to happen.
Compile-time safety isn’t enough—Zod closes the runtime gap⌗
If you stop at TypeScript inference, you still have a runtime problem: the network doesn’t care about your types. A malicious client (or a buggy client) can send invalid data.
tRPC typically pairs with Zod (or similar validators) so you get the best of both worlds:
- Compile-time types: derived from the Zod schema and the procedure.
- Runtime validation: enforced on the server before your logic runs.
That means your server doesn’t just hope the input matches—it checks it. And your client doesn’t just trust the types—it’s guided by them.
Practical takeaway: validate at the boundary, type everywhere⌗
A pattern I now treat as non-negotiable:
- Use Zod to define the input schema.
- Let tRPC infer the TypeScript types from that schema.
- Keep business logic focused on valid inputs, not defensive parsing everywhere.
Your procedure becomes easier to read because you remove a layer of clutter—“validate this, check that, handle weird shapes”—and move it into a clear schema definition.
Eliminating “contract maintenance” changes how you ship⌗
Once the contract is shared automatically, development velocity improves in a way that’s hard to quantify but easy to feel.
You refactor without fear⌗
In traditional REST setups, refactoring a request or response means juggling:
- update backend code
- update OpenAPI docs (if you have them)
- regenerate client types (if you have them)
- audit call sites across the app
- coordinate deployments so clients don’t break
With tRPC, the “audit across the app” part becomes the compiler’s job. When you change the server procedure signature, the client sees it immediately. You fix broken call sites as you go, and you don’t ship until everything compiles cleanly.
You don’t design contracts separately from code⌗
REST often leads you to think: “First we define the API. Then we implement it.” That separation sounds tidy, but it creates drift. tRPC collapses that workflow. You build the procedure, define the validation, define the return type, and the client just follows.
This has a subtle effect: API design becomes less of a heavyweight phase and more of an iterative practice.
REST and GraphQL feel like ceremony when TypeScript does the syncing⌗
tRPC also changes the emotional comparison. Without code generation, without schemas-as-docs, without “versioned contracts,” REST and GraphQL start to feel like you’re doing extra work to keep two systems aligned.
REST still has strengths—caching semantics, broad ecosystem support, and a universal language for HTTP clients. GraphQL still offers flexible querying and tooling around schemas and resolvers.
But if your world is already TypeScript end-to-end, tRPC brings the contract problem to a stop.
The monorepo advantage is real⌗
In a TypeScript monorepo, the server and client often evolve together. tRPC turns that advantage into a workflow: shared types, inferred procedure signatures, and a unified developer experience.
If you’re working across package boundaries, you also get a practical enforcement mechanism: changes to the server router break the client build immediately. That’s the kind of feedback loop you want—fast, local, and unavoidable.
Where tRPC fits best (and where you should still be careful)⌗
Let’s be honest: you shouldn’t declare REST irrelevant in every context. The value of tRPC is strongest when:
- your stack is TypeScript-heavy
- backend and frontend live in the same repo (or at least share types through a package boundary)
- you care deeply about developer experience and integration correctness
- your API surface benefits from procedure-level modeling
You still need to think about compatibility for external consumers. If third-party clients must integrate, you may still need public, language-agnostic contracts (OpenAPI, documentation, or a published schema). tRPC doesn’t magically replace that. It excels at the internal “frontend ↔ backend” relationship where TypeScript can be the contract layer.
Also, don’t treat runtime validation as optional. Type inference doesn’t validate network inputs; Zod does.
Conclusion: stop negotiating contracts—make them compile⌗
tRPC “made me forget REST exists” not because HTTP disappeared, but because the contract stopped being fragile. When the backend procedure definition and the frontend usage share types through inference, you eliminate drift by construction. Add Zod validation and you get runtime safety too.
If your team lives in TypeScript and you want fewer moving parts—no OpenAPI file, no codegen pipeline, fewer version mismatch headaches—tRPC is one of the rare upgrades that changes both the reliability and the feel of building APIs.