tRPC gives you end-to-end type safety between your frontend and backend without code generation or schemas. But most developers only use basic queries and mutations.
The Free APIs You're Missing
1. Subscriptions — Real-Time Data with WebSockets
// server
import { observable } from "@trpc/server/observable";
import { EventEmitter } from "events";
const ee = new EventEmitter();
const appRouter = router({
onNewMessage: publicProcedure.subscription(() => {
return observable<Message>((emit) => {
const handler = (msg: Message) => emit.next(msg);
ee.on("message", handler);
return () => ee.off("message", handler);
});
}),
sendMessage: publicProcedure
.input(z.object({ text: z.string() }))
.mutation(({ input }) => {
const msg = { id: crypto.randomUUID(), text: input.text };
ee.emit("message", msg);
return msg;
}),
});
// client
const { data } = trpc.onNewMessage.useSubscription(undefined, {
onData: (msg) => console.log("New message:", msg),
});
2. Middleware — Type-Safe Request Pipeline
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
return next({ ctx: { user: ctx.user } }); // user is now non-null in procedures
});
const isAdmin = t.middleware(({ ctx, next }) => {
if (ctx.user.role !== "admin") throw new TRPCError({ code: "FORBIDDEN" });
return next({ ctx: { user: ctx.user, isAdmin: true as const } });
});
const protectedProcedure = t.procedure.use(isAuthed);
const adminProcedure = protectedProcedure.use(isAdmin);
3. Output Validation — Ensure API Contracts
const userRouter = router({
getUser: publicProcedure
.input(z.object({ id: z.string() }))
.output(z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
// password field is automatically stripped!
}))
.query(({ input }) => {
return db.user.findUnique({ where: { id: input.id } });
}),
});
Output validation strips extra fields. Your password hash never leaks to the client.
4. Server-Side Calls — Call Procedures Without HTTP
const caller = appRouter.createCaller({
user: { id: "system", role: "admin" },
});
// Call procedures directly — no HTTP overhead
const users = await caller.user.list();
const report = await caller.analytics.generate({ period: "monthly" });
Perfect for cron jobs, queue workers, and seed scripts.
5. Links — Custom Request/Response Pipeline
import { httpBatchLink, loggerLink, splitLink, wsLink } from "@trpc/client";
const client = createTRPCClient<AppRouter>({
links: [
loggerLink({ enabled: () => process.env.NODE_ENV === "development" }),
splitLink({
condition: (op) => op.type === "subscription",
true: wsLink({ url: "ws://localhost:3001" }),
false: httpBatchLink({ url: "http://localhost:3000/api/trpc" }),
}),
],
});
Getting Started
npm install @trpc/server @trpc/client zod
Need data from any website delivered as clean JSON? I build production web scrapers that handle anti-bot, proxies, and rate limits. 77 scrapers running in production. Email me: Spinov001@gmail.com
Check out my awesome-web-scraping list for the best scraping tools and resources.
Top comments (0)