DEV Community

Alex Spinov
Alex Spinov

Posted on

tRPC Has a Free RPC Framework: Full-Stack Type Safety Without Code Generation or Schema Files

GraphQL needs schemas, resolvers, codegen, and a client library. REST needs OpenAPI specs, validation middleware, and typed fetch wrappers. Both require you to define your API contract twice — once on the server, once on the client.

What if your client automatically knew every API endpoint's input and output types — with zero code generation?

That's tRPC. Define a function on the server. Call it from the client. TypeScript handles the rest.

How It Works

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

const t = initTRPC.create();

export const appRouter = t.router({
  getUser: t.procedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      const user = await db.user.findUnique({ where: { id: input.id } });
      return user; // { id: string, name: string, email: string }
    }),

  createUser: t.procedure
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }))
    .mutation(async ({ input }) => {
      return db.user.create({ data: input });
    }),
});

export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode
// client/page.tsx — full type inference, zero codegen
import { trpc } from "../utils/trpc";

function UserProfile({ userId }: { userId: string }) {
  const { data: user } = trpc.getUser.useQuery({ id: userId });
  // user is typed: { id: string, name: string, email: string } | undefined

  const createUser = trpc.createUser.useMutation();

  const handleCreate = () => {
    createUser.mutate({ 
      name: "Aleksej",
      email: "dev@example.com",
      // TypeScript error if you add unknown fields or wrong types
    });
  };

  return <div>{user?.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Change a field name on the server → TypeScript immediately shows every broken client call. No API docs to update. No types to regenerate.

Setup With Next.js

// server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";

const t = initTRPC.create({ transformer: superjson });

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
  if (!ctx.session?.user) throw new TRPCError({ code: "UNAUTHORIZED" });
  return next({ ctx: { user: ctx.session.user } });
});
Enter fullscreen mode Exit fullscreen mode
// server/routers/posts.ts
export const postsRouter = router({
  list: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).default(10),
      cursor: z.string().optional(),
    }))
    .query(async ({ input }) => {
      const posts = await db.post.findMany({
        take: input.limit + 1,
        cursor: input.cursor ? { id: input.cursor } : undefined,
        orderBy: { createdAt: "desc" },
      });

      let nextCursor: string | undefined;
      if (posts.length > input.limit) {
        nextCursor = posts.pop()!.id;
      }

      return { posts, nextCursor };
    }),

  create: protectedProcedure
    .input(z.object({
      title: z.string().min(1).max(200),
      content: z.string().min(1),
    }))
    .mutation(async ({ input, ctx }) => {
      return db.post.create({
        data: { ...input, authorId: ctx.user.id },
      });
    }),
});
Enter fullscreen mode Exit fullscreen mode

Infinite Scroll — Built In

function PostFeed() {
  const { data, fetchNextPage, hasNextPage } = trpc.posts.list.useInfiniteQuery(
    { limit: 10 },
    { getNextPageParam: (lastPage) => lastPage.nextCursor }
  );

  return (
    <div>
      {data?.pages.flatMap(p => p.posts).map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      {hasNextPage && <button onClick={() => fetchNextPage()}>Load more</button>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

tRPC vs GraphQL vs REST

Feature tRPC GraphQL REST
Type safety Automatic Needs codegen Manual
Schema file None Required OpenAPI (optional)
Setup complexity Low Medium Low
Over/under-fetching N/A (RPC) Solved Common problem
Learning curve Low (just TS) Medium Low
Non-TS clients No Yes Yes

When to Choose tRPC

Choose tRPC when:

  • Frontend and backend are both TypeScript
  • Your team owns both client and server code
  • You want the fastest possible API development cycle
  • Type safety is more important than API format standards

Skip tRPC when:

  • You have non-TypeScript clients (mobile apps, third-party integrations)
  • You need a public API (REST or GraphQL is more standard)
  • You want to use different languages for frontend and backend

The Bottom Line

tRPC eliminates the API layer as a source of bugs. When your types flow from database → server → client without a single manual type definition, entire categories of errors become impossible.

Start here: trpc.io


Need custom data extraction, scraping, or automation? I build tools that collect and process data at scale — 78 actors on Apify Store and 265+ open-source repos. Email me: Spinov001@gmail.com | My Apify Actors

Top comments (0)