DEV Community

Cover image for I Spent Two Weeks Replacing REST With tRPC. Here's What Nobody Tells You.
Emma Schmidt
Emma Schmidt

Posted on

I Spent Two Weeks Replacing REST With tRPC. Here's What Nobody Tells You.

I've written more REST endpoints than I can count. Request comes in, I write a DTO, I write a validator, I write matching types on the frontend, I pray the two never drift apart. Then last month I finally gave in and rebuilt a mid-sized internal tool using tRPC instead, mostly because half of my dev.to feed wouldn't shut up about it.

Two weeks later I get it. But not for the reasons everyone's tweeting about.

The Problem That Started This

Here's the exact bug that pushed me over the edge. Someone renamed a field on the backend from userId to accountId. The API still returned 200. The frontend still rendered. It just quietly showed undefined everywhere that field was used, and nobody noticed until a customer emailed asking why their dashboard looked broken.

That's the failure mode REST allows by default: a contract that only exists in your head and in a documentation page nobody reads. Nothing crashes, nothing throws, it just silently lies to you.

The Pitch Everyone Makes

The usual sales pitch goes like this:

  • No more manually syncing types between frontend and backend
  • No more API documentation going stale the week after you write it
  • No more runtime surprises because your types are shared end to end
  • Change a field on the server, TypeScript yells at you on the client before you even save the file

All true. None of it is the actual reason I'd recommend it.

What Actually Changed My Mind

The real shift wasn't the type safety itself. It was how much dead weight quietly disappeared from the codebase once I stopped needing it.

Here's what I deleted from the project by the end of week two:

  • The OpenAPI spec file, which was already three fields behind reality
  • The separate client SDK generation step that ran in CI and occasionally just failed for no reason
  • The Postman collection that three people updated and forty people ignored
  • A folder of hand-written TypeScript interfaces that existed only to describe what the API returned
// server/router.ts
export const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return db.user.findUnique({ where: { id: input.id } });
    }),
});

export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode
// client/api.ts
import { createTRPCClient } from '@trpc/client';
import type { AppRouter } from '../server/router';

const client = createTRPCClient<AppRouter>({ url: '/api/trpc' });

const user = await client.getUser.query({ id: '123' });
// user is fully typed, no codegen step, no .d.ts file to regenerate
Enter fullscreen mode Exit fullscreen mode

That's it. That's the whole integration. No schema file to keep in sync, no build step to remember to run, no "did anyone regenerate the types after that last deploy" Slack message.

Before and After, Side by Side

This is the part that actually sold the rest of my team, so I'll lay it out plainly.

Renaming a field on the backend, REST version:

  1. Update the field in the database model
  2. Update the endpoint response shape
  3. Update the OpenAPI spec, if anyone remembers
  4. Regenerate the client SDK, if the CI step doesn't silently fail
  5. Manually update every frontend type that referenced the old field
  6. Find out three weeks later that one component was missed

Renaming a field on the backend, tRPC version:

  1. Update the field in the database model
  2. Save the file
  3. Every frontend usage that references the old field name turns red in your editor immediately

That second list isn't shorter because I'm exaggerating for effect. It's shorter because two entire categories of human error just stopped being possible.

Where It Actually Falls Apart

Here's the part the hype posts conveniently skip.

tRPC only works when both sides speak TypeScript. The moment any of these show up, the whole pitch changes:

  • A mobile team building in Swift or Kotlin
  • A partner integration that needs a documented, language-agnostic contract
  • A public API with external developers who've never seen your codebase

In every one of those cases you're back to needing REST or GraphQL with real documentation, because "just import the types" doesn't work across a language boundary or a company boundary.

The migration cost is real, and it's not a weekend project. Here's what actually ate my two weeks:

  • Every existing fetch call needed rewriting, not just the obvious ones
  • Every error handling pattern had to be redone since tRPC errors don't look like REST errors
  • Every loading and retry state in the UI needed a second look
  • One legacy endpoint that three other internal tools depended on had to keep working in REST form the entire time

If you're planning this migration, budget for the boring rewrite work, not just the exciting type-safety demo.

A Quick Gut Check Before You Migrate

Ask yourself these before touching a single endpoint:

  • Does my frontend and backend both live in TypeScript, ideally the same repo?
  • Does one team own both sides, or will a separate mobile or partner team need this API too?
  • Am I trying to fix a real, recurring bug pattern, or just chasing what's trending this month?
  • Can I migrate one feature at a time instead of doing a big bang rewrite?

If you answered yes to the first two and no to the third, you're the exact use case tRPC was built for.

The Actual Verdict

For an internal dashboard, an admin panel, or any full stack app where one team owns both ends, it's genuinely worth the switch. For anything with external consumers, keep your REST or GraphQL layer and don't let a trend talk you out of a decision that was already correct.

The pattern that seems to be winning in 2026 isn't picking one paradigm forever, it's running both side by side on purpose:

  • tRPC for the internal application layer, where your own frontend talks to your own backend
  • REST for webhooks, third-party integrations, and anything public facing

Two systems, each doing the job it's actually good at, instead of forcing one tool to cover every use case.

Would I Do It Again

Yes, but only on projects where I'd draw the same boundary I just described. The type safety is nice. The disappearance of an entire category of "the docs are out of date again" bugs is the part that actually saved me time, and saved me from that one silent undefined bug that started this whole thing.

Have you made the jump yet, or are you still holding the line with REST? Curious where everyone's landing on this one, drop your setup in the comments.

Top comments (0)