tRPC in 2026: Full-Stack TypeScript Without the Boilerplate
If you've ever wasted an afternoon keeping your frontend types in sync with your backend API, tRPC is the library that will feel like a revelation.
In 2026, tRPC has become a staple of the modern TypeScript full-stack stack — sitting alongside Next.js, Prisma, and Supabase as a must-know tool for solo devs and small teams.
What Is tRPC?
tRPC stands for TypeScript Remote Procedure Call. The idea is simple:
Define your backend functions once. Call them from the frontend with full type safety — no code generation, no REST schemas, no GraphQL resolvers.
The result? A single source of truth for your entire app's API contract.
The Problem tRPC Solves
With traditional REST APIs:
- You define a route on the backend:
GET /api/users/:id - You write a TypeScript interface on the frontend:
interface User { ... } - You pray they stay in sync
They never do. Someone changes the backend, forgets to update the frontend type, and you get a runtime error that only surfaces in production.
With GraphQL, you solve the type problem but add huge complexity (resolvers, SDL, codegen).
tRPC gives you GraphQL-level type safety with zero overhead.
How tRPC Works (The Simple Version)
// server.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return await db.user.findUnique({ where: { id: input.id } });
}),
createPost: t.procedure
.input(z.object({ title: z.string(), content: z.string() }))
.mutation(async ({ input }) => {
return await db.post.create({ data: input });
}),
});
export type AppRouter = typeof appRouter;
// client.ts (Next.js page or component)
import { trpc } from '../utils/trpc';
function UserProfile({ userId }: { userId: string }) {
// Fully typed — TypeScript knows the shape of 'user'
const { data: user } = trpc.getUser.useQuery({ id: userId });
return <div>{user?.name}</div>; // No manual type annotation needed
}
Notice: no manual types exported, no code generation step. The AppRouter type does all the work.
tRPC vs REST vs GraphQL in 2026
| REST | GraphQL | tRPC | |
|---|---|---|---|
| Type safety | ❌ Manual | ✅ (with codegen) | ✅ Automatic |
| Setup complexity | Low | High | Low |
| Runtime overhead | Low | Medium | Low |
| Learning curve | Easy | Hard | Easy |
| Best for | Public APIs | Large teams | Full-stack TS apps |
tRPC shines when you control both frontend and backend — which is most solo projects and startups.
The Modern tRPC Stack in 2026
Most developers combine tRPC with:
- Next.js (App Router or Pages Router)
- Prisma (ORM for database queries)
- Zod (runtime input validation — tRPC requires it)
- NextAuth / Clerk (authentication)
- Supabase or PlanetScale (database)
This stack is sometimes called T3 Stack (created by Theo Browne), and it has become one of the most popular starting points for production-grade Next.js apps.
Setting Up tRPC in a Next.js App (2026 Version)
1. Install dependencies
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
2. Initialize tRPC
// src/server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { ZodError } from 'zod';
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session?.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { ...ctx, session: ctx.session } });
});
3. Create your router
// src/server/routers/post.ts
import { z } from 'zod';
import { router, protectedProcedure, publicProcedure } from '../trpc';
export const postRouter = router({
getAll: publicProcedure.query(async ({ ctx }) => {
return ctx.db.post.findMany({ orderBy: { createdAt: 'desc' } });
}),
create: protectedProcedure
.input(z.object({ title: z.string().min(1).max(100), content: z.string() }))
.mutation(async ({ ctx, input }) => {
return ctx.db.post.create({
data: { ...input, authorId: ctx.session.user.id },
});
}),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
return ctx.db.post.delete({ where: { id: input.id } });
}),
});
Real-World Error Handling
One of tRPC's strengths is typed error handling:
import { TRPCClientError } from '@trpc/client';
try {
await trpc.post.create.mutate({ title: '', content: 'test' });
} catch (error) {
if (error instanceof TRPCClientError) {
// TypeScript knows the error shape
const zodError = error.data?.zodError;
console.log(zodError?.fieldErrors); // { title: ['String must contain at least 1 character'] }
}
}
No more any error types or guessing the error shape at runtime.
When NOT to Use tRPC
tRPC is not for everything:
- Public APIs consumed by third parties → use REST or GraphQL
- Microservices with different tech stacks → REST/gRPC
- Mobile apps with native codebases → REST is easier to integrate
- Teams with backend specialists who don't know TypeScript → REST
If you're building a product solo or with a small TypeScript team? tRPC is almost always the right call.
5 tRPC Patterns Worth Knowing
1. Input validation with Zod
.input(z.object({
email: z.string().email(),
age: z.number().min(18).max(120),
}))
2. Middleware for auth
const authMiddleware = t.middleware(({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { user: ctx.user } });
});
3. Optimistic updates
const utils = trpc.useContext();
mutation.mutate(data, {
onSuccess: () => utils.post.getAll.invalidate(),
});
4. Subscriptions (WebSockets)
subscription: t.procedure.subscription(({ ctx }) => {
return observable<Message>((emit) => {
// WebSocket logic here
});
}),
5. Batching requests
tRPC automatically batches multiple queries into a single HTTP request — zero config.
The Verdict
tRPC in 2026 is mature, production-proven, and has a thriving ecosystem. If you're building a TypeScript full-stack app and you're not using it, you're writing a lot of unnecessary boilerplate.
The learning curve is small (especially if you already know TypeScript and Zod), and the productivity gains are immediate.
Start with the T3 Stack, deploy to Vercel, and ship your project. You can always move to REST or GraphQL if your requirements change — but 90% of projects never need to.
Building a freelance business or side project? The Freelancer OS Notion Template gives you a complete CRM, project tracker, and income dashboard — all in Notion. €19, one-time purchase.
Top comments (0)