DEV Community

Alex Spinov
Alex Spinov

Posted on

tRPC Has a Free API You're Not Using

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),
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 } });
    }),
});
Enter fullscreen mode Exit fullscreen mode

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" });
Enter fullscreen mode Exit fullscreen mode

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" }),
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Getting Started

npm install @trpc/server @trpc/client zod
Enter fullscreen mode Exit fullscreen mode

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)