DEV Community

Alex Spinov
Alex Spinov

Posted on

tRPC v11 Has a Free API That Eliminates the API Layer Between Frontend and Backend

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

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

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

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

Build type-safe data APIs? My Apify tools integrate with any tRPC backend.

Custom tRPC solution? Email spinov001@gmail.com

Top comments (0)