DEV Community

Otto
Otto

Posted on

tRPC in 2026: Full-Stack TypeScript Without the Boilerplate

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:

  1. You define a route on the backend: GET /api/users/:id
  2. You write a TypeScript interface on the frontend: interface User { ... }
  3. 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;
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 } });
});
Enter fullscreen mode Exit fullscreen mode

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 } });
    }),
});
Enter fullscreen mode Exit fullscreen mode

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'] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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),
}))
Enter fullscreen mode Exit fullscreen mode

2. Middleware for auth

const authMiddleware = t.middleware(({ ctx, next }) => {
  if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next({ ctx: { user: ctx.user } });
});
Enter fullscreen mode Exit fullscreen mode

3. Optimistic updates

const utils = trpc.useContext();
mutation.mutate(data, {
  onSuccess: () => utils.post.getAll.invalidate(),
});
Enter fullscreen mode Exit fullscreen mode

4. Subscriptions (WebSockets)

subscription: t.procedure.subscription(({ ctx }) => {
  return observable<Message>((emit) => {
    // WebSocket logic here
  });
}),
Enter fullscreen mode Exit fullscreen mode

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)