Routers
Organizing tRPC routers
Creating a Router
Routers organize related procedures into feature-based modules.
Basic Router Structure
// src/trpc/procedures/routers/my-resource.ts
import { createTRPCRouter } from "@/trpc/init";
import { authProcedure } from "@/trpc/procedures/trpc";
import { z } from "zod";
export const myResourceRouter = createTRPCRouter({
list: authProcedure
.meta({ rateLimit: "QUERY" })
.query(async ({ ctx }) => {
return {
success: true,
payload: await ctx.db.myResource.list(),
};
}),
get: authProcedure
.meta({ rateLimit: "QUERY" })
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
return {
success: true,
payload: await ctx.db.myResource.get({ id: input.id }),
};
}),
create: authProcedure
.meta({ rateLimit: "MUTATION" })
.input(myResourceSchema)
.mutation(async ({ ctx, input }) => {
const result = await ctx.db.myResource.create(input);
return {
success: true,
message: ctx.t("toasts.saved"),
payload: result,
};
}),
});Combining Routers
Routers are combined in the root router to create the full API surface.
Root Router
// src/trpc/procedures/root.ts
import { createTRPCRouter } from "@/trpc/init";
import { myResourceRouter } from "./routers/my-resource";
import { anotherRouter } from "./routers/another-resource";
export const appRouter = createTRPCRouter({
myResource: myResourceRouter,
another: anotherRouter,
});
export type AppRouter = typeof appRouter;Usage
// Client-side
const { data } = api.myResource.list.useQuery();
const { data } = api.another.get.useQuery({ id: "123" });
// Server-side
const data = await api.myResource.list();Router Organization
Feature-Based Structure
Organize routers by feature or domain:
src/trpc/procedures/routers/
├── auth.ts # Authentication operations
├── users.ts # User management
├── organizations.ts # Organization operations
├── api-keys.ts # API key management
└── subscriptions.ts # Subscription managementNested Routers
For complex features, create nested routers:
export const organizationRouter = createTRPCRouter({
// Basic operations
list: authProcedure.query(/* ... */),
get: authProcedure.input(/* ... */).query(/* ... */),
// Nested router for members
members: createTRPCRouter({
list: authProcedure.query(/* ... */),
invite: authProcedure.mutation(/* ... */),
remove: authProcedure.mutation(/* ... */),
}),
});Usage:
const { data } = api.organization.list.useQuery();
const { data } = api.organization.members.list.useQuery();Best Practices
1. Consistent Response Format
Always return ActionResponse:
return {
success: true,
message: ctx.t("toasts.saved"),
payload: data,
} satisfies ActionResponse;2. Input Validation
Use Zod schemas for input validation:
import { z } from "zod";
const createSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
});
create: authProcedure
.input(createSchema)
.mutation(async ({ input }) => {
// input is fully typed and validated
})3. Rate Limiting
Add rate limiting to all procedures:
list: authProcedure
.meta({ rateLimit: "QUERY" })
.query(/* ... */),
create: authProcedure
.meta({ rateLimit: "MUTATION" })
.mutation(/* ... */),4. Context Usage
Leverage context for common operations:
export const myRouter = createTRPCRouter({
list: authProcedure.query(async ({ ctx }) => {
// ctx.user - Current user (available in authProcedure)
// ctx.db - Database operations
// ctx.t - Translations
// ctx.headers - Request headers
// ctx.session - Current session
return await ctx.db.myResource.list({
where: { userId: ctx.user.id },
});
}),
});Template
For step-by-step instructions on creating a new tRPC router, see the New tRPC Router Template.