Marketplace Starter Kit

API (tRPC)

Learn how the app's API is structured using tRPC routers and procedures.

Project Structure

The API routes live inside:

src/trpc/
├── routers/
│   ├── auth.ts
│   ├── booking.ts
│   ├── listing.ts
│   └── index.ts         # Main router
├── context.ts
└── server.ts

Each file in routers/ defines a domain-specific procedure group (e.g. listing, booking, auth).


Creating a Router

Each router exports procedures using publicProcedure or protectedProcedure:

import { publicProcedure, router } from "../trpc";

export const listingRouter = router({
  getAll: publicProcedure.query(async ({ ctx }) => {
    return ctx.db.listing.findMany();
  }),
});

Protecting Routes

Use protectedProcedure to require authentication:

import { protectedProcedure } from "../trpc";

export const bookingRouter = router({
  create: protectedProcedure
    .input(BookingSchema)
    .mutation(async ({ ctx, input }) => {
      return ctx.db.booking.create({ data: input });
    }),
});

This automatically validates the session context and throws if the user is not authenticated.


Validating Inputs with Zod

tRPC integrates with Zod for input validation:

const BookingSchema = z.object({
  listingId: z.string().uuid(),
  startTime: z.date(),
  endTime: z.date(),
});

Accessing API in the Frontend

You call API procedures directly from the client:

const { data, isLoading } = trpc.listing.getAll.useQuery();

Or mutate data:

const createBooking = trpc.booking.create.useMutation();

Composing the Main Router

The root index.ts file inside routers/ composes all routers together:

import { router } from "../trpc";
import { authRouter } from "./auth";
import { bookingRouter } from "./booking";
import { listingRouter } from "./listing";

export const appRouter = router({
  auth: authRouter,
  booking: bookingRouter,
  listing: listingRouter,
});

export type AppRouter = typeof appRouter;

Context & Middleware

Global context (session, db access, etc.) is defined in src/trpc/context.ts:

export async function createContext(opts: CreateNextContextOptions) {
  const session = await getServerAuthSession(opts.req, opts.res);
  return {
    session,
    db,
  };
}

Use middleware for role-based access:

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

Error Handling

Use TRPCError to throw custom errors:

throw new TRPCError({
  code: "BAD_REQUEST",
  message: "Invalid booking window",
});

This integrates with your frontend error boundary or toasts.


Testing tRPC Handlers

You can test your tRPC procedures in isolation using:

import { appRouter } from "~/trpc/routers";
import { createCaller } from "~/trpc/server";

const caller = appRouter.createCaller(mockContext);
await caller.booking.create({ listingId: "id", startTime: ..., endTime: ... });

Summary

  • Use publicProcedure for open access, protectedProcedure for auth-required routes.
  • Structure routers by domain.
  • Co-locate validation (Zod) and access control (middleware).
  • Auto-infer API types from backend to frontend.
  • tRPC is fully framework-integrated, eliminating the need for REST or GraphQL.

Explore the src/trpc/routers/ directory for available procedures.