tRPC v11 lets you call backend functions from the frontend with zero API layer — full type safety, no codegen, no schemas.
Define Your API: Procedures
import { initTRPC, TRPCError } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.context<Context>().create();
const appRouter = t.router({
products: t.router({
list: t.procedure
.input(z.object({
category: z.string().optional(),
minPrice: z.number().optional(),
maxPrice: z.number().optional(),
page: z.number().default(1),
}))
.query(async ({ input }) => {
return db.product.findMany({
where: {
category: input.category,
price: { gte: input.minPrice, lte: input.maxPrice },
},
skip: (input.page - 1) * 20,
take: 20,
});
}),
create: t.procedure
.input(z.object({
title: z.string().min(1),
price: z.number().positive(),
url: z.string().url(),
}))
.mutation(async ({ input }) => {
return db.product.create({ data: input });
}),
delete: t.procedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input, ctx }) => {
if (!ctx.user?.isAdmin) throw new TRPCError({ code: "FORBIDDEN" });
return db.product.delete({ where: { id: input.id } });
}),
}),
});
export type AppRouter = typeof appRouter;
Client: Type-Safe Calls
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../server/router";
export const trpc = createTRPCReact<AppRouter>();
function ProductList() {
const { data, isLoading } = trpc.products.list.useQuery({
category: "electronics",
maxPrice: 100,
});
const createMutation = trpc.products.create.useMutation({
onSuccess: () => utils.products.list.invalidate(),
});
if (isLoading) return <Spinner />;
return (
<div>
{data?.map(p => <div key={p.id}>{p.title}: ${p.price}</div>)}
<button onClick={() => createMutation.mutate({ title: "New", price: 29.99, url: "https://..." })}>
Add Product
</button>
</div>
);
}
Subscriptions: Real-Time
// Server
priceUpdates: t.procedure.subscription(() => {
return observable<PriceUpdate>((emit) => {
const onUpdate = (data: PriceUpdate) => emit.next(data);
priceEmitter.on("update", onUpdate);
return () => priceEmitter.off("update", onUpdate);
});
}),
// Client
trpc.priceUpdates.useSubscription(undefined, {
onData: (update) => console.log("Price changed:", update),
});
Middleware: Auth, Logging, Rate Limiting
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);
// All procedures using protectedProcedure require auth
const adminRouter = t.router({
deleteUser: protectedProcedure
.input(z.object({ userId: z.string() }))
.mutation(({ input, ctx }) => {
// ctx.user is guaranteed to exist
return db.user.delete({ where: { id: input.userId } });
}),
});
Build type-safe data APIs? My Apify tools integrate with any tRPC backend.
Custom tRPC solution? Email spinov001@gmail.com
Top comments (0)