DEV Community

Alex Spinov
Alex Spinov

Posted on

tRPC Has a Free API — Here's How to Build End-to-End Type-Safe APIs

tRPC lets you build fully type-safe APIs without schemas, code generation, or runtime validation overhead. Types flow from your backend to frontend automatically.

Installation

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
Enter fullscreen mode Exit fullscreen mode

Server Setup

import { initTRPC } from "@trpc/server";
import { z } from "zod";

const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;

const appRouter = router({
  // Query — get data
  hello: publicProcedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      return { greeting: `Hello, ${input.name}!` };
    }),

  // Query — list items
  posts: publicProcedure
    .input(z.object({ limit: z.number().default(10) }))
    .query(async ({ input }) => {
      return db.post.findMany({ take: input.limit, orderBy: { createdAt: "desc" } });
    }),

  // Mutation — create item
  createPost: publicProcedure
    .input(z.object({
      title: z.string().min(1).max(200),
      content: z.string().min(10)
    }))
    .mutation(async ({ input }) => {
      return db.post.create({ data: input });
    }),

  // Mutation — delete
  deletePost: publicProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ input }) => {
      await db.post.delete({ where: { id: input.id } });
      return { success: true };
    })
});

export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode

Client (React)

import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "./server";

const trpc = createTRPCReact<AppRouter>();

function Posts() {
  const posts = trpc.posts.useQuery({ limit: 10 });
  const createPost = trpc.createPost.useMutation({
    onSuccess: () => posts.refetch()
  });

  if (posts.isLoading) return <p>Loading...</p>;

  return (
    <div>
      <button onClick={() => createPost.mutate({ title: "New", content: "Hello world!" })}>
        Create Post
      </button>
      {posts.data?.map(post => <div key={post.id}>{post.title}</div>)}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Middleware

const isAuthed = t.middleware(({ next, ctx }) => {
  if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
  return next({ ctx: { user: ctx.user } });
});

const protectedProcedure = t.procedure.use(isAuthed);

const appRouter = router({
  secretData: protectedProcedure.query(({ ctx }) => {
    return { secret: "Only for authenticated users", user: ctx.user.name };
  })
});
Enter fullscreen mode Exit fullscreen mode

Why tRPC?

  • Zero code generation — types flow automatically
  • Full autocomplete — Ctrl+Space shows all available procedures
  • Runtime validation — Zod schemas validate at runtime
  • Tiny bundle — no schema overhead on client

Need to extract or automate web content at scale? Check out my web scraping tools on Apify — no coding required. Or email me at spinov001@gmail.com for custom solutions.

Top comments (0)