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.tsEach 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
publicProcedurefor open access,protectedProcedurefor 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.