Development··3 min read

My Experience Building Type-Safe APIs with tRPC

What it was like switching from REST to tRPC, including the good parts and painful mistakes over 3 months

When a Type Error Blew Up in Production

Friday, 4:37 PM. A 500 error hit production. The cause? The frontend was sending userName but the backend expected username. One letter of capitalization. Nobody caught it for two weeks. (The test environment was using different mock data, so it never surfaced there.)

This kind of thing kept happening with our REST API. Type mismatches between frontend and backend were causing 3-4 production bugs per quarter. That's when we decided to adopt tRPC.

The Setup That Took Way Too Long

We set up tRPC v11. Combining it with Next.js App Router was rough because the docs were thin on that topic. (More accurately, the App Router section had an "experimental" warning slapped on it.) Setup took two and a half days. I genuinely thought it would take half a day.

The biggest headache was calling tRPC from server-side code. To invoke procedures from RSC, you need createCaller, and this pattern was buried deep in the documentation. Stack Overflow answers were mostly for v10, which was subtly different.

// This doesn't work, but it's what I tried first
const data = await trpc.user.getById.query({ id: 1 });
 
// This is what you actually need
const caller = createCaller(createContext);
const data = await caller.user.getById({ id: 1 });

Okay But It Actually Is Really Nice

Once setup is done, development speed genuinely picks up. Define a router on the backend and autocomplete lights up on the frontend. Input types, output types, everything. No more manually writing API specs.

Before this, we maintained Swagger docs by hand, and they'd fall out of sync constantly. Especially when response fields were added or changed to nullable. With tRPC, changing a Zod schema immediately triggers type errors on the frontend. Caught at compile time, not runtime.

Over 3 months, we had zero production bugs related to type mismatches. (Down from 3-4 per quarter. That's a meaningful difference.)

Where Things Started Getting Messy

The problem is that tRPC can't replace everything REST does. Webhook endpoints for external service integrations can't be built with tRPC. It's designed for client-server communication, so you still need REST endpoints for incoming HTTP requests from external sources.

So now the project has both tRPC routers and REST API routes living side by side. It's honestly a bit messy. Every time a new team member asks "where do I send this request?" I feel a little guilty.

File uploads were tricky too. tRPC doesn't natively handle multipart form data. We ended up creating a separate REST endpoint for uploads. As these exceptions piled up, there were moments of "was tRPC really the right call?"

Still Wouldn't Go Back Though

After 3 months, the conclusion is that the benefits outweigh the drawbacks. Type safety alone makes it worth it. Once you've experienced red squiggly lines appearing in your frontend the moment a backend response type changes, you can't go back.

But I wouldn't recommend it for every project. If you have lots of external API integrations or if most of your team is deeply familiar with REST, the learning curve is significant. Two out of four team members took over a month to get comfortable. (One of them still occasionally submits PRs with REST endpoints.)

Those two and a half days of setup were painful, but the debugging time saved since then is roughly 47 hours. Definitely got my money's worth.

Related Posts