Documentation

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/procedures

Step 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:

packages/api/modules/example/procedures/create-example.ts
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:

packages/api/modules/example/router.ts
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:

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 access

Route 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 structure

2. 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/:id

5. Type Safety

Export and reuse types:

// types.ts
export type ExampleInput = z.infer<typeof exampleInputSchema>;

// Use in other files
import type \{ ExampleInput \} from "./types";