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:
- Your frontend and backend are in the same monorepo
- You're using TypeScript end-to-end
- You're a solo developer or small team who doesn't need to expose a public API
It's not a good fit when:
- You have mobile clients or third-party consumers who need a proper REST or GraphQL API
- Your team has dedicated backend engineers who aren't TypeScript-first
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:
- Install
@trpc/server,@trpc/client,@trpc/next, andzod - Create a root router in
server/trpc.ts - Move API routes one by one into tRPC procedures
- Replace
fetchcalls on the frontend withtrpc.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.