DEV Community

Alex Spinov
Alex Spinov

Posted on

tRPC Has a Free API — End-to-End TypeScript APIs Without Code Generation

tRPC lets you build fully type-safe APIs without schemas, code generation, or REST conventions. Define a function on the server — call it from the client with full autocompletion.

Why tRPC?

  • Zero code generation — types flow automatically from server to client
  • Full autocompletion — IntelliSense for every API call
  • No REST — no routes, no HTTP methods, no serialization headaches
  • Works with — Next.js, Nuxt, SvelteKit, Express, Fastify

Quick Start

npm install @trpc/server @trpc/client @trpc/react-query
Enter fullscreen mode Exit fullscreen mode

Server

// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.context<Context>().create();

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

export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);
export const router = t.router;
Enter fullscreen mode Exit fullscreen mode
// server/routers/users.ts
export const userRouter = router({
  list: publicProcedure.query(async () => {
    return await db.select().from(users);
  }),

  byId: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      return await db.select().from(users).where(eq(users.id, input.id));
    }),

  create: protectedProcedure
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }))
    .mutation(async ({ input }) => {
      return await db.insert(users).values(input).returning();
    }),

  update: protectedProcedure
    .input(z.object({
      id: z.number(),
      name: z.string().optional(),
      email: z.string().email().optional(),
    }))
    .mutation(async ({ input: { id, ...data } }) => {
      return await db.update(users).set(data).where(eq(users.id, id)).returning();
    }),
});
Enter fullscreen mode Exit fullscreen mode

Client (React Query)

import { trpc } from '../utils/trpc';

function UserList() {
  const { data: users, isLoading } = trpc.user.list.useQuery();
  const createUser = trpc.user.create.useMutation({
    onSuccess: () => utils.user.list.invalidate(),
  });

  if (isLoading) return <p>Loading...</p>;

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

Subscriptions (Real-Time)

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

// Client
trpc.onNewMessage.useSubscription(undefined, {
  onData: (message) => {
    setMessages((prev) => [...prev, message]);
  },
});
Enter fullscreen mode Exit fullscreen mode

Next.js App Router

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  });

export { handler as GET, handler as POST };
Enter fullscreen mode Exit fullscreen mode

Error Handling

import { TRPCError } from '@trpc/server';

deleteUser: protectedProcedure
  .input(z.object({ id: z.number() }))
  .mutation(async ({ input, ctx }) => {
    const user = await db.select().from(users).where(eq(users.id, input.id));
    if (!user) throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
    if (user.id !== ctx.user.id) throw new TRPCError({ code: 'FORBIDDEN' });
    await db.delete(users).where(eq(users.id, input.id));
  }),
Enter fullscreen mode Exit fullscreen mode

Building type-safe data APIs? Check out my Apify actors for structured web data, or email spinov001@gmail.com for custom tRPC solutions.

tRPC, REST, or GraphQL — which API style do you prefer? Comment below!

Top comments (0)