trpctypescriptapinextjsfullstack
Why I Switched from REST to tRPC (And You Should Too)
Type-safe APIs without the boilerplate. Here's how tRPC transformed my development workflow and eliminated an entire class of bugs.
December 16, 20257 min read
Why I Switched from REST to tRPC (And You Should Too)
I've built dozens of REST APIs. I've written OpenAPI specs, generated clients, and debugged countless type mismatches between frontend and backend. Then I discovered tRPC, and I'm never going back.
The Problem with REST
REST is great, but it has friction:
typescript
// Backend: Define your endpoint
app.get("/api/users/:id", async (req, res) => {
const user = await db.user.findUnique({ where: { id: req.params.id } });
res.json(user);
});
// Frontend: Hope the types match
const response = await fetch("/api/users/123");
const user = await response.json(); // type: any 😱You can fix this with:
- OpenAPI/Swagger specs
- Code generation
- Shared type packages
But that's a lot of ceremony for something that should be simple.
Enter tRPC
tRPC gives you end-to-end type safety with zero code generation:
typescript
// Backend: Define your router
const appRouter = router({
user: router({
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input.id } });
}),
}),
});
export type AppRouter = typeof appRouter;
// Frontend: Full type inference!
const user = await trpc.user.byId.query({ id: "123" });
// TypeScript knows exactly what `user` containsReal Benefits I've Experienced
1. Catch Errors at Compile Time
typescript
// This won't compile - TypeScript catches the error
const user = await trpc.user.byId.query({ userId: "123" });
// ^^^^^^
// Error: Object literal may only specify known properties2. Autocomplete Everything
Your IDE knows every procedure, every input field, every return type. No more guessing or checking docs.
3. Refactoring is Safe
Rename a field on the backend? TypeScript shows you every frontend usage that needs updating.
4. Validation Built-In
With Zod integration, your inputs are validated automatically:
typescript
const createUser = publicProcedure
.input(z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
age: z.number().min(13).optional(),
}))
.mutation(async ({ input }) => {
// input is fully typed AND validated
return db.user.create({ data: input });
});When NOT to Use tRPC
tRPC isn't for everything:
- Public APIs - REST/GraphQL are better for third-party consumers
- Non-TypeScript clients - The magic requires TypeScript on both ends
- Microservices - tRPC shines in monorepos
My Setup
typescript
// server/trpc.ts
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
const t = initTRPC.create({
transformer: superjson, // Handles Dates, Maps, Sets, etc.
});
export const router = t.router;
export const publicProcedure = t.procedure;typescript
// app/api/trpc/[trpc]/route.ts (Next.js App Router)
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers/_app";
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
});
export { handler as GET, handler as POST };The Verdict
| Aspect | REST | tRPC |
|---|---|---|
| Type Safety | Manual | Automatic |
| Boilerplate | High | Low |
| Learning Curve | Low | Medium |
| Flexibility | High | Medium |
| Best For | Public APIs | Internal APIs |
For full-stack TypeScript apps, tRPC is a game-changer. The productivity boost from end-to-end type safety is worth the learning curve.
Building something with tRPC? I'd love to hear about it. Get in touch.