DEV Community

Atlas Whoff
Atlas Whoff

Posted on

tRPC: End-to-End Type Safety Without REST or GraphQL

tRPC: End-to-End Type Safety Without REST or GraphQL

tRPC lets you call server functions from your client as if they were local functions — fully typed, no code generation, no schema files.

If your frontend and backend are both TypeScript, tRPC eliminates an entire class of bugs: the mismatch between what your API returns and what your UI expects.

How It Works

You define procedures on the server:

// server/router.ts
import { z } from 'zod';
import { router, publicProcedure } from './trpc';

export const appRouter = router({
  users: router({
    list: publicProcedure
      .input(z.object({ limit: z.number().default(10) }))
      .query(async ({ input }) => {
        return db.users.findMany({ take: input.limit });
      }),

    create: publicProcedure
      .input(z.object({ name: z.string(), email: z.string().email() }))
      .mutation(async ({ input }) => {
        return db.users.create({ data: input });
      }),
  }),
});

export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode

Then call them from the client with full autocomplete:

// client/UserList.tsx
import { trpc } from '../utils/trpc';

function UserList() {
  // Fully typed — TS knows the shape of the response
  const { data: users } = trpc.users.list.useQuery({ limit: 20 });

  const createUser = trpc.users.create.useMutation();

  return (
    <div>
      {users?.map(user => <div key={user.id}>{user.name}</div>)}
      <button onClick={() => createUser.mutate({ name: 'Alice', email: 'alice@example.com' })}>
        Add User
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

No REST endpoints. No fetch calls. No any types. If the server changes the response shape, TypeScript errors immediately in the client.

Setup

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
Enter fullscreen mode Exit fullscreen mode
// server/trpc.ts
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;
Enter fullscreen mode Exit fullscreen mode
// client/utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../server/router';

export const trpc = createTRPCReact<AppRouter>();
Enter fullscreen mode Exit fullscreen mode

Protected Procedures

Middleware lets you add authentication to procedures:

const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({ ctx: { session: ctx.session } });
});

export const protectedProcedure = t.procedure.use(isAuthed);
Enter fullscreen mode Exit fullscreen mode

When Not to Use tRPC

tRPC works best when:

  • Both client and server are TypeScript
  • They live in the same monorepo (or share types)
  • You don't need a public API

If external clients (mobile apps, third-party integrations) need to consume your API, REST or GraphQL is better — tRPC doesn't have a good story for non-TypeScript consumers.

The Full Stack Picture

tRPC pairs beautifully with Next.js, Prisma, and NextAuth for a fully type-safe full-stack TypeScript app. This exact stack — plus Stripe for billing — is what powers the AI SaaS Starter Kit. Skip the setup and start building your actual product.

Top comments (0)