API Router
STOP before writing any router.
- New routers MUST use
createDb()against a Drizzle table. users.tsandfiles.tsare NOT templates. They call platform endpoints purpose-built for theusersandfilesplatform features — there is no Drizzle table behind them. Your application's entities are application data and go through your own tables; do not copyusers.ts/files.tsfor any new entity.- If your entity has no Drizzle table yet, invoke
schema0-db-schemafirst to define one.
File Location
packages/api/src/
├── index.ts # Base procedures (publicProcedure, protectedProcedure)
├── context.ts # Request context type
└── routers/
├── index.ts # App router (register all routers here)
└── [entity].ts # Entity-specific CRUD routersPrerequisites
Every router provides 5 bulk CRUD operations: selectAll, selectById, insertMany, updateMany, deleteMany.
Base Procedures
import { ORPCError, os } from "@orpc/server";
import type { Context } from "./context";
export const o = os.$context<Context>();
export const publicProcedure = o;
const requireAuth = o.middleware(async ({ context, next }) => {
if (!context.session?.user) {
throw new ORPCError("UNAUTHORIZED");
}
return next({ context: { session: context.session } });
});
export const protectedProcedure = publicProcedure.use(requireAuth);Error Handling (ORPCError)
import { ORPCError } from "@orpc/server";
// CORRECT -- string code + options object
throw new ORPCError("NOT_FOUND", { status: 404, message: "Resource not found" });
throw new ORPCError("CONFLICT", { status: 409, message: "Already exists" });
// WRONG -- object with code property
throw new ORPCError({ code: "NOT_FOUND", message: "Not found" });Nested Routers
// packages/api/src/routers/admin/users.ts
export const adminUsersRouter = { list: protectedProcedure.handler(async () => { ... }) };
// packages/api/src/routers/admin/index.ts
export const adminRouter = { users: adminUsersRouter };
// packages/api/src/routers/index.ts
export const appRouter = { admin: adminRouter };
// Client: orpc.admin.users.list.queryOptions()Key Rules
- Use
.handler()for ALL procedures -- NEVER.query()or.mutation()(tRPC patterns). - Use
createDb()inside each handler -- never module-level singleton. - NEVER use
fetchCustomResourcesfor new routers -- only for built-infiles.tsandusers.ts. - Use
import {z} from "zod/v4"-- NEVERimport z from "zod". - Import schemas from
@template/db/schema-- NEVER definez.object()schemas inline. - Use
{entity}RouterOutputSchemafor.output()-- must match DB return types. - Bulk operations only --
insertMany,updateMany,deleteManyfor TanStack DB optimistic updates. - Data MUST be stored in database -- every mutation must hit DB and return persisted result.
- Register in
packages/api/src/routers/index.tsafter creating.
References
references/crud-template.md-- Full CRUD router code template (selectAll, selectById, insertMany, updateMany, deleteMany) + registration example