Create oRPC Router
Step-by-step template for adding a new oRPC router
Prerequisites
Decide these first:
- Router name (
<feature>sRouter, e.g.projectsRouter). - Procedure set (
list,getById,upsert,delete, etc.). - Access model (
publicProcedure,authProcedure, orroleProcedure).
1. Create the Router File
Create src/rpc/procedures/routers/<feature>.ts:
import {
<feature>DeleteSchema,
<feature>GetSchema,
<feature>UpsertSchema,
} from "@/features/<feature>/schema";
import {
authProcedure,
createRPCRouter,
} from "@/rpc/procedures/rpc";
import { type ActionResponse } from "@/lib/utils/schema-utils";
export const <feature>sRouter = createRPCRouter({
list: authProcedure
.meta({ rateLimit: "QUERY" })
.query(async ({ ctx }) => {
const items = await ctx.db.<feature>s.getByOwnerId(ctx.user.id);
return {
success: true,
payload: items,
} satisfies ActionResponse;
}),
getById: authProcedure
.meta({ rateLimit: "QUERY" })
.input(<feature>GetSchema)
.query(async ({ ctx, input }) => {
const item = await ctx.db.<feature>s.get(input.id);
return {
success: true,
payload: item,
} satisfies ActionResponse;
}),
upsert: authProcedure
.meta({ rateLimit: "MUTATION" })
.input(<feature>UpsertSchema)
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input;
if (id) {
const updated = await ctx.db.<feature>s.update(id, data);
return {
success: true,
message: ctx.t("toasts.saved"),
payload: updated,
} satisfies ActionResponse;
}
const created = await ctx.db.<feature>s.create({
...data,
ownerId: ctx.user.id,
});
return {
success: true,
message: ctx.t("toasts.saved"),
payload: created,
} satisfies ActionResponse;
}),
delete: authProcedure
.meta({ rateLimit: "MUTATION" })
.input(<feature>DeleteSchema)
.mutation(async ({ ctx, input }) => {
await ctx.db.<feature>s.remove(input.id);
return {
success: true,
message: ctx.t("toasts.deleted"),
} satisfies ActionResponse;
}),
});2. Register in Root Router
Update src/rpc/procedures/root.ts:
import { <feature>sRouter } from "@/rpc/procedures/routers/<feature>";
export const appRouter = createRPCRouter({
// ...existing
<feature>s: <feature>sRouter,
});3. Add OpenAPI Exposure (Optional)
If the procedure must be REST-accessible, add .route(...) and explicit schemas:
import { actionResponseSchema } from "@/lib/utils/schema-utils";
import { z } from "zod";
list: authProcedure
.meta({ rateLimit: "QUERY", acceptApiKey: true })
.route({
method: "GET",
path: "/<feature>s",
tags: ["<feature>s"],
summary: "List <feature>s",
})
.input(z.object({}).optional())
.output(actionResponseSchema(z.array(<feature>ApiSchema)))
.query(async ({ ctx }) => {
const items = await ctx.db.<feature>s.getByOwnerId(ctx.user.id);
return { success: true, payload: items };
});4. Rate Limit Keys
"QUERY"for reads"MUTATION"for writes"AUTH"for auth operations"ADMIN"for admin operationsfalseto disable
5. Post-Creation Checklist
- Router file created in
src/rpc/procedures/routers/ - Router registered in
src/rpc/procedures/root.ts - Inputs validated with Zod
- Procedure return type follows
ActionResponse - Rate limits set with
.meta({ rateLimit: ... }) - Optional
.route(...)added for REST exposure