Procedures
Creating tRPC procedures
Procedure Types
Different procedure types provide different levels of authentication and authorization:
| Procedure | Description | Use Case |
|---|---|---|
publicProcedure | No authentication required | Public data, health checks |
authProcedure | Requires valid session | User-specific operations |
roleProcedure(role) | Requires specific role | Admin operations |
Public Procedure
No authentication required:
import { publicProcedure } from "@/trpc/procedures/trpc";
export const healthRouter = createTRPCRouter({
ping: publicProcedure.query(() => {
return { status: "ok" };
}),
});Auth Procedure
Requires a valid session. The user is automatically available in ctx.user:
import { authProcedure } from "@/trpc/procedures/trpc";
export const profileRouter = createTRPCRouter({
getProfile: authProcedure.query(async ({ ctx }) => {
// ctx.user is guaranteed to be non-null
const userId = ctx.user.id;
return await ctx.db.users.get({ id: userId });
}),
});Role Procedure
Requires a specific role or roles:
import { roleProcedure } from "@/trpc/procedures/trpc";
import { UserRole } from "@/db/enums";
export const adminRouter = createTRPCRouter({
// Single role
listUsers: roleProcedure(UserRole.ADMIN).query(async ({ ctx }) => {
return ctx.db.users.list();
}),
// Multiple roles allowed
moderateContent: roleProcedure([UserRole.ADMIN, UserRole.MODERATOR])
.mutation(async ({ ctx }) => {
// ...
}),
});Rate Limiting
Rate limiting is configured per procedure via .meta():
list: authProcedure
.meta({ rateLimit: "QUERY" })
.query(async ({ ctx }) => { ... })Rate Limit Keys
| Rate Limit Key | Use Case |
|---|---|
"QUERY" | Read operations |
"MUTATION" | Write operations |
"AUTH" | Authentication operations |
false | Disable rate limiting |
Configure limits in src/config/app.ts under Config.RateLimit.
ActionResponse Pattern
All procedures should return ActionResponse for consistent error handling:
import type { ActionResponse } from "@/types/action-response";
export const myRouter = createTRPCRouter({
create: authProcedure
.input(createSchema)
.mutation(async ({ ctx, input }) => {
const result = await ctx.db.myResource.create(input);
return {
success: true,
message: ctx.t("toasts.saved"),
payload: result,
} satisfies ActionResponse;
}),
});Error Handling
For errors that can be recovered, return ActionResponse:
return {
success: false,
message: ctx.t("customErrors.organization.invitation-invalid"),
} satisfies ActionResponse;If execution cannot continue, throw a CustomError:
import { CustomError } from "@/lib/errors";
throw new CustomError("customErrors.generic.error");CustomError keys are in src/messages/dictionaries/<locale>/customErrors.json. See Error Handling for more info.
Creating a Procedure
Basic Example
import { authProcedure } from "@/trpc/procedures/trpc";
import { z } from "zod";
export const myRouter = createTRPCRouter({
list: authProcedure
.meta({ rateLimit: "QUERY" })
.query(async ({ ctx }) => {
const data = await ctx.db.myResource.list({
where: { userId: ctx.user.id },
});
return {
success: true,
payload: data,
} satisfies ActionResponse;
}),
create: authProcedure
.meta({ rateLimit: "MUTATION" })
.input(z.object({
name: z.string().min(1),
description: z.string().optional(),
}))
.mutation(async ({ ctx, input }) => {
const result = await ctx.db.myResource.create({
...input,
userId: ctx.user.id,
});
return {
success: true,
message: ctx.t("toasts.saved"),
payload: result,
} satisfies ActionResponse;
}),
});