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;
// 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>;
}
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 } });
});
// 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 },
});
}),
});
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>
);
}
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)