Back to blog
Next.jsTypeScriptNode.js

Why I Switched from REST to tRPC in My Next.js Projects

After building a dozen REST APIs, I gave tRPC a proper shot. Here's what changed, what didn't, and whether it's worth the switch for solo developers.

November 15, 20243 min read

Building APIs is the part of full-stack development nobody talks about honestly. You write the endpoint, you write the types on the frontend, and then six months later you change one field name on the backend and spend two hours tracking down every broken frontend reference.

tRPC doesn't solve everything. But it solves that.

The problem with REST + TypeScript

When you build a REST API in Express or Next.js API routes, your types live in two places. The backend knows what it returns. The frontend hopes it knows. You're either manually keeping types in sync, using a code-gen tool like OpenAPI, or just using any and accepting the consequences.

I did all three of those at different points in my career.

// The old way — backend
app.get('/api/user/:id', async (req, res) => {
  const user = await db.user.findById(req.params.id);
  res.json(user); // TypeScript has no idea what this looks like on the frontend
});

// Frontend — just hope it matches
const user = await fetch('/api/user/123').then(r => r.json()); // type: any

This works fine until it doesn't. And when it breaks, it breaks silently at runtime, not at compile time.

What tRPC actually gives you

tRPC is not a protocol. It's a library that lets you call backend functions from the frontend as if they were local functions — with full end-to-end type safety, no code generation, no schema files.

// Server — define your router
const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return db.user.findById(input.id);
    }),
});

// Client — call it like a local function
const user = await trpc.user.getById.query({ id: '123' });
// user is fully typed — autocomplete, type errors, everything

Your IDE knows the shape of user before you even run the app.

When tRPC makes sense

tRPC is a great fit when:

It's not a good fit when:

The migration process

I migrated one of my existing Next.js + Express projects over a weekend. The hardest part wasn't the code — it was fighting my own muscle memory for how I'd structured things.

The steps were roughly:

  1. Install @trpc/server, @trpc/client, @trpc/next, and zod
  2. Create a root router in server/trpc.ts
  3. Move API routes one by one into tRPC procedures
  4. Replace fetch calls on the frontend with trpc.xxx.query()

The end result was about 30% less code and zero type sync issues since.

Final verdict

If you're building a Next.js app where the API is only consumed by that same Next.js frontend, tRPC is worth learning. The initial setup takes an afternoon. The payoff is every subsequent feature you build faster because you're not maintaining two sets of types.

If you're building a public API or need mobile clients — stick with REST and use OpenAPI for code gen. tRPC isn't the right tool there.

For solo full-stack projects though? It's become my default.

All posts