DEV Community

Alex Spinov
Alex Spinov

Posted on

tRPC Has a Free API: End-to-End TypeScript Type Safety Without Code Generation

What is tRPC?

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 TypeScript autocompletion and type checking — as if calling a local function.

Why tRPC?

  • Free and open-source — MIT license
  • Zero code generation — no GraphQL schemas, no OpenAPI specs
  • End-to-end type safety — change server → client instantly knows
  • Autocompletion — IDE shows available procedures and their types
  • Framework agnostic — Next.js, React, Vue, Svelte, anything
  • Subscriptions — real-time via WebSockets built in

Quick Start

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

Server (Define Your API)

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

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;

// server/routers/users.ts
export const userRouter = router({
  getAll: publicProcedure.query(async () => {
    return db.select().from(users);
  }),

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

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

// server/index.ts
export const appRouter = router({
  users: userRouter,
  posts: postRouter,
});

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

Client (Full Type Safety)

// React component with tRPC
import { trpc } from '../utils/trpc';

function UserList() {
  // TypeScript KNOWS the return type
  const { data: users, isLoading } = trpc.users.getAll.useQuery();

  const createUser = trpc.users.create.useMutation({
    onSuccess: () => utils.users.getAll.invalidate()
  });

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

  return (
    <div>
      {users?.map(user => (
        <div key={user.id}>{user.name} - {user.email}</div>
      ))}
      <button onClick={() => createUser.mutate({
        name: 'Alex',
        email: 'alex@example.com'
        // TypeScript catches: email: 123 ← TYPE ERROR
      })}>
        Add User
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Middleware (Auth, Logging)

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

const protectedProcedure = t.procedure.use(isAuthed);

export const adminRouter = router({
  getStats: protectedProcedure.query(async ({ ctx }) => {
    // ctx.user is guaranteed to exist and typed
    return getStatsForUser(ctx.user.id);
  }),
});
Enter fullscreen mode Exit fullscreen mode

Real-Time Subscriptions

// Server
const postRouter = router({
  onNew: publicProcedure.subscription(() => {
    return observable<Post>((emit) => {
      const onAdd = (post: Post) => emit.next(post);
      eventEmitter.on('newPost', onAdd);
      return () => eventEmitter.off('newPost', onAdd);
    });
  }),
});

// Client
trpc.posts.onNew.useSubscription(undefined, {
  onData(post) {
    console.log('New post:', post.title);
  }
});
Enter fullscreen mode Exit fullscreen mode

tRPC vs Alternatives

Feature tRPC REST + OpenAPI GraphQL Hono RPC
Type safety End-to-end Generated client Generated types End-to-end
Code generation None Required Required None
Learning curve Very low Low High Low
Over/under-fetching No issue Over-fetching Solves it No issue
Caching React Query Manual Apollo Cache Manual
Multi-language TypeScript only Any Any TypeScript only

Real-World Impact

A team maintaining a REST API spent 30% of their time keeping OpenAPI specs and generated clients in sync. Stale types caused runtime errors in production twice a month. After migrating to tRPC: zero type mismatches, TypeScript catches breaking changes at compile time, and developers ship features 40% faster because they never wonder "what does this endpoint return?"


Building full-stack TypeScript applications? I help teams implement type-safe architectures. Contact spinov001@gmail.com or explore my data tools on Apify.

Top comments (0)