Create a new API router
Learn how to create and register new API routers in your application.
Creating a New Router
Follow these steps to create a new API module:
Step 1: Create Module Directory
Create a new module directory under packages/api/modules/:
mkdir -p packages/api/modules/example/proceduresStep 2: Define Types and Schemas
Create types.ts to define your data validation rules using Zod:
// packages/api/modules/example/types.ts
import \{ z \} from "zod";
export const exampleInputSchema = z.object(\{
name: z.string().min(3).max(100),
email: z.string().email(),
age: z.number().min(18).optional(),
tags: z.array(z.string()).optional(),
\});
export const exampleOutputSchema = z.object(\{
id: z.string(),
name: z.string(),
email: z.string(),
age: z.number().optional(),
tags: z.array(z.string()),
createdAt: z.date(),
\});
export type ExampleInput = z.infer<typeof exampleInputSchema>;
export type ExampleOutput = z.infer<typeof exampleOutputSchema>;Step 3: Create Procedures
Create individual API endpoint implementations in the procedures/ directory:
import \{ publicProcedure \} from "../../../orpc/procedures";
import \{ exampleInputSchema, exampleOutputSchema \} from "../types";
export const createExample = publicProcedure
.route(\{
method: "POST",
path: "/example",
tags: ["Example"],
summary: "Create a new example",
description: "Creates a new example resource with the provided data",
\})
.input(exampleInputSchema)
.output(exampleOutputSchema)
.handler(async (\{ input \}) => \{
// Your business logic here
const result = \{
id: generateId(),
...input,
tags: input.tags ?? [],
createdAt: new Date(),
\};
return result;
\});Step 4: Create Module Router
Create router.ts to export all procedures in your module:
import \{ createExample \} from "./procedures/create-example";
import \{ getExample \} from "./procedures/get-example";
import \{ updateExample \} from "./procedures/update-example";
import \{ deleteExample \} from "./procedures/delete-example";
export const exampleRouter = \{
create: createExample,
get: getExample,
update: updateExample,
delete: deleteExample,
\};Step 5: Register in Main Router
Add your module to the main router in packages/api/orpc/router.ts:
import \{ exampleRouter \} from "../modules/example/router";
export const router = publicProcedure
.prefix("/api")
.router(\{
admin: adminRouter,
users: usersRouter,
example: exampleRouter, // Add your new router
// ... other routers
\});Using Your API
Frontend (RPC)
Call your API with full type safety:
import \{ createApiClient \} from "@/lib/api-client";
const client = createApiClient();
// TypeScript knows the exact input/output types
const result = await client.example.create(\{
name: "John Doe",
email: "john@example.com",
age: 25,
tags: ["developer", "typescript"],
\});
console.log(result.id); // Type-safe accessRoute Configuration
Available Options
When defining a route with .route(), you can configure:
.route(\{
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH",
path: "/your-path", // API endpoint path
tags: ["YourTag"], // OpenAPI tags for documentation
summary: "Short description", // Brief summary
description: "Full details", // Detailed description
deprecated: false, // Mark as deprecated
\})Path Parameters
Support dynamic path parameters:
export const getExample = publicProcedure
.route(\{
method: "GET",
path: "/example/:id",
tags: ["Example"],
summary: "Get example by ID",
\})
.input(z.object(\{
id: z.string(),
\}))
.handler(async (\{ input \}) => \{
const example = await findById(input.id);
return example;
\});Query Parameters
Handle query parameters:
export const listExamples = publicProcedure
.route(\{
method: "GET",
path: "/examples",
tags: ["Example"],
summary: "List all examples",
\})
.input(z.object(\{
page: z.number().default(1),
limit: z.number().default(10),
search: z.string().optional(),
\}))
.handler(async (\{ input \}) => \{
const examples = await findMany(\{
page: input.page,
limit: input.limit,
search: input.search,
\});
return examples;
\});Middleware
Using Middleware
Add middleware to your procedures:
import \{ localeMiddleware \} from "../../../orpc/middleware/locale-middleware";
export const localizedAction = publicProcedure
.use(localeMiddleware)
.route(\{
method: "POST",
path: "/localized-action",
\})
.handler(async (\{ context \}) => \{
// Access locale from context
const locale = context.locale;
return \{ message: getTranslation(locale) \};
\});Creating Custom Middleware
Create your own middleware:
// packages/api/orpc/middleware/custom-middleware.ts
import \{ os \} from "@orpc/server";
export const customMiddleware = os
.$context<\{ headers: Headers \}>()
.middleware(async (\{ context, next \}) => \{
// Pre-processing
const startTime = Date.now();
// Execute next middleware/handler
const result = await next(\{
context: \{
customData: "value",
\},
\});
// Post-processing
const duration = Date.now() - startTime;
console.log(`Request took $\{duration\}ms`);
return result;
\});Best Practices
1. Schema Validation
Always define input and output schemas:
.input(inputSchema) // Validates incoming data
.output(outputSchema) // Documents response structure2. Descriptive Routes
Provide clear documentation:
.route(\{
summary: "Clear, concise summary",
description: "Detailed explanation of what this endpoint does",
tags: ["Organized", "Categorized"],
\})3. Error Handling
Handle errors gracefully (see Error Handling):
.handler(async (\{ input \}) => \{
if (!isValid(input)) \{
throw new ORPCError("BAD_REQUEST", \{
message: "Validation failed",
\});
\}
// ... rest of logic
\})4. Module Organization
Keep related procedures together:
modules/example/
├── router.ts # Export all procedures
├── types.ts # Shared schemas
└── procedures/
├── create-example.ts # POST /example
├── get-example.ts # GET /example/:id
├── list-examples.ts # GET /examples
├── update-example.ts # PUT /example/:id
└── delete-example.ts # DELETE /example/:id5. Type Safety
Export and reuse types:
// types.ts
export type ExampleInput = z.infer<typeof exampleInputSchema>;
// Use in other files
import type \{ ExampleInput \} from "./types";