DEV Community

Alex Spinov
Alex Spinov

Posted on

tRPC Has a Free Library That Gives You End-to-End Type Safety — No REST, No GraphQL, No Codegen

The API Problem

REST: you guess the response shape. GraphQL: you write schemas, resolvers, and run codegen. Both: your frontend and backend types drift apart.

tRPC shares types directly between server and client. Change a return type on the server → your frontend gets a type error instantly. No codegen. No schemas.

What tRPC Gives You

Define a Router

import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

const appRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      const user = await db.users.findById(input.id);
      return user; // TypeScript knows the exact shape
    }),

  createPost: t.procedure
    .input(z.object({
      title: z.string().min(1),
      content: z.string(),
    }))
    .mutation(async ({ input }) => {
      return db.posts.create(input);
    }),
});

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

Call From the Client

import { createTRPCClient } from '@trpc/client';
import type { AppRouter } from '../server';

const trpc = createTRPCClient<AppRouter>({ /* config */ });

// Full autocomplete + type checking
const user = await trpc.getUser.query({ id: '123' });
console.log(user.name); // TypeScript knows this exists

const post = await trpc.createPost.mutate({
  title: 'Hello',
  content: 'World',
});
Enter fullscreen mode Exit fullscreen mode

React Integration

function UserProfile({ id }: { id: string }) {
  const { data, isLoading } = trpc.getUser.useQuery({ id });

  if (isLoading) return <p>Loading...</p>;
  return <h1>{data?.name}</h1>; // Fully typed
}
Enter fullscreen mode Exit fullscreen mode

Subscriptions (WebSockets)

// Server
onNewMessage: t.procedure.subscription(() => {
  return observable<Message>((emit) => {
    const onMessage = (msg: Message) => emit.next(msg);
    ee.on('message', onMessage);
    return () => ee.off('message', onMessage);
  });
}),

// Client
trpc.onNewMessage.subscribe(undefined, {
  onData(message) {
    console.log('New message:', message);
  },
});
Enter fullscreen mode Exit fullscreen mode

Middleware

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

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

Works With

Next.js, Remix, Nuxt, SvelteKit, Express, Fastify, Bun, Deno — any TypeScript stack.

Why This Matters

If your frontend and backend are both TypeScript, you don't need REST or GraphQL. tRPC gives you a function call that happens to cross the network — with full type safety.


Building type-safe APIs that need web data? Check out my web scraping actors on Apify Store — structured data via API. For custom solutions, email spinov001@gmail.com.

Top comments (0)