# How to Add a New API Route to OmniRoute Following Its Standard Pattern

> Learn to add a new API route to OmniRoute by exporting an OPTIONS handler, validating requests with Zod, and delegating logic to handlers. Follow the standard pattern for seamless integration.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-08-21

---

**To add a new API route to OmniRoute, you must export an OPTIONS handler that returns global CORS headers, validate request bodies using Zod schemas centralized in [`src/shared/validation/schemas.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/validation/schemas.ts), optionally authenticate requests via helpers in [`src/sse/services/auth.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/sse/services/auth.ts), delegate core logic to handlers under `open-sse/handlers/`, and route all errors through `buildErrorBody()` from [`open-sse/utils/error.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/utils/error.ts) to prevent stack trace leakage.**

OmniRoute enforces a strict, reusable structure for every HTTP endpoint to guarantee type-safety and consistent security posture. The diegosouzapw/OmniRoute repository documents this exact architecture in [`docs/architecture/CODEBASE_DOCUMENTATION.md`](https://github.com/diegosouzapw/OmniRoute/blob/main/docs/architecture/CODEBASE_DOCUMENTATION.md), and you can observe the implementation in the existing entry point at [`src/app/api/v1/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/route.ts). Following this standard pattern ensures your new endpoint handles pre-flight requests, input validation, and error sanitization identically to the rest of the application.

## The Five-Step Route Creation Pattern

Every route in OmniRoute follows a mandatory five-step flow. Deviating from this pattern breaks CORS handling or exposes internal error details to clients.

### Step 1: Implement CORS Pre-Flight Handling

Every route file must export an `OPTIONS` handler that returns the global `CORS_HEADERS` defined in [`src/shared/utils/cors.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/utils/cors.ts). This handles browser pre-flight requests before the actual method calls.

```typescript
import { CORS_HEADERS } from "@/shared/utils/cors";

export async function OPTIONS() {
  return new Response(null, { headers: CORS_HEADERS });
}

```

OmniRoute requires this explicit export in every [`route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/route.ts) file to ensure consistent cross-origin behavior across all API versions.

### Step 2: Define Zod Input Validation

Request bodies must be validated using Zod schemas stored in [`src/shared/validation/schemas.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/validation/schemas.ts). This central location prevents schema duplication and guarantees type-safety throughout the application.

```typescript
import { z } from "zod";

export const myFeatureSchema = z.object({
  prompt: z.string().min(1),
  maxTokens: z.number().int().positive().default(256),
  temperature: z.number().min(0).max(2).default(0.7),
});

```

Your route handler imports this schema and calls `.parse()` on the incoming JSON body. Zod throws on invalid payloads, which you catch and route through the error sanitizer.

### Step 3: Add Optional Authentication

If your route requires an API key or OAuth token, import the shared authentication helpers from [`src/sse/services/auth.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/sse/services/auth.ts). Use `extractApiKey()` to parse the header and `isValidApiKey()` to verify credentials.

```typescript
import { extractApiKey, isValidApiKey } from "@/sse/services/auth.ts";

// Inside your POST handler:
const apiKey = await extractApiKey(request);
if (!isValidApiKey(apiKey)) throw new Error("Invalid API key");

```

Public endpoints skip this step, but internal or management routes should enforce authentication before delegating to business logic. For management-only routes, also register the path in [`src/shared/constants/publicApiRoutes.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/constants/publicApiRoutes.ts) to control public surface exposure.

### Step 4: Delegate to a Core Handler

Keep the route file thin by delegating business logic to handlers under `open-sse/handlers/`. This separation isolates streaming concerns from HTTP transport details.

```typescript
import { myFeatureHandler } from "@/open-sse/handlers/myFeatureHandler";

export async function POST(request: Request) {
  const body = await request.json();
  const parsed = myFeatureSchema.parse(body);
  return await myFeatureHandler(parsed);
}

```

Handlers like `handleChatCore` or `handleEmbeddingCore` reside in this directory and return standard `Response` objects that the route forwards to the client.

### Step 5: Sanitize Errors with `buildErrorBody`

Never expose stack traces or internal error details to API consumers. Import `buildErrorBody()` from [`open-sse/utils/error.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/utils/error.ts) and wrap your entire handler logic in a try-catch block.

```typescript
import { buildErrorBody } from "@/open-sse/utils/error";

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const parsed = myFeatureSchema.parse(body);
    return await myFeatureHandler(parsed);
  } catch (err: any) {
    return new Response(buildErrorBody(err), {
      status: err.status ?? 400,
      headers: { "Content-Type": "application/json", ...CORS_HEADERS },
    });
  }
}

```

The `buildErrorBody()` function strips sensitive internal details while preserving user-friendly error messages required for debugging.

## Complete Implementation Example

Create your route at [`src/app/api/my-feature/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/my-feature/route.ts) following this full skeleton:

```typescript
import { CORS_HEADERS } from "@/shared/utils/cors";
import { myFeatureSchema } from "@/shared/validation/schemas";
import { buildErrorBody } from "@/open-sse/utils/error";
import { myFeatureHandler } from "@/open-sse/handlers/myFeatureHandler";

/**
 * CORS pre-flight
 */
export async function OPTIONS() {
  return new Response(null, { headers: CORS_HEADERS });
}

/**
 * POST /api/my-feature
 * – validates payload with Zod
 * – delegates to the core handler
 * – sanitizes all errors
 */
export async function POST(request: Request) {
  try {
    const body = await request.json();
    const parsed = myFeatureSchema.parse(body);

    // Optional: Add auth check here
    // const apiKey = await extractApiKey(request);
    // if (!isValidApiKey(apiKey)) throw new Error("Invalid API key");

    return await myFeatureHandler(parsed);
  } catch (err: any) {
    return new Response(buildErrorBody(err), {
      status: err.status ?? 400,
      headers: { "Content-Type": "application/json", ...CORS_HEADERS },
    });
  }
}

```

Define your handler in [`open-sse/handlers/myFeatureHandler.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/handlers/myFeatureHandler.ts):

```typescript
import { getExecutor } from "@/open-sse/executors/registry";

export async function myFeatureHandler(payload: {
  prompt: string;
  maxTokens: number;
  temperature: number;
}) {
  const executor = getExecutor("openai");
  const response = await executor.execute({
    model: "gpt-4o-mini",
    messages: [{ role: "user", content: payload.prompt }],
    max_tokens: payload.maxTokens,
    temperature: payload.temperature,
  });
  return new Response(JSON.stringify(response), {
    status: 200,
    headers: { "Content-Type": "application/json", ...CORS_HEADERS },
  });
}

```

## Testing Your New Route

OmniRoute requires unit tests for all routes per the repository's hard rule #8. Create a test file at [`tests/unit/myFeature.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tests/unit/myFeature.test.ts) to verify validation and error handling:

```typescript
import { describe, it, expect } from "vitest";
import { POST } from "@/src/app/api/my-feature/route";

describe("POST /api/my-feature", () => {
  it("rejects invalid payload", async () => {
    const badReq = new Request("http://test/api/my-feature", {
      method: "POST",
      body: JSON.stringify({ prompt: "" }), // fails min(1)
    });
    const res = await POST(badReq);
    expect(res.status).toBe(400);
    const json = await res.json();
    expect(json.error.message).toContain("prompt");
  });
});

```

Update [`docs/reference/API_REFERENCE.md`](https://github.com/diegosouzapw/OmniRoute/blob/main/docs/reference/API_REFERENCE.md) and [`docs/openapi.yaml`](https://github.com/diegosouzapw/OmniRoute/blob/main/docs/openapi.yaml) to expose the new endpoint to external consumers.

## Summary

- **CORS handling**: Every [`route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/route.ts) must export an `OPTIONS` handler returning `CORS_HEADERS` from [`src/shared/utils/cors.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/utils/cors.ts).
- **Input validation**: Centralize Zod schemas in [`src/shared/validation/schemas.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/validation/schemas.ts) to enforce type-safety and protect against malformed payloads.
- **Authentication**: Use `extractApiKey()` and `isValidApiKey()` from [`src/sse/services/auth.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/sse/services/auth.ts) for protected routes.
- **Handler delegation**: Place core business logic in `open-sse/handlers/` to keep route files focused on transport concerns.
- **Error sanitization**: Always pass errors through `buildErrorBody()` from [`open-sse/utils/error.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/utils/error.ts) to prevent stack trace exposure.
- **Documentation**: Update [`CODEBASE_DOCUMENTATION.md`](https://github.com/diegosouzapw/OmniRoute/blob/main/CODEBASE_DOCUMENTATION.md), API reference docs, and the OpenAPI specification when adding public routes.

## Frequently Asked Questions

### Where should I place the Zod schema for my new route?

Place all Zod schemas in [`src/shared/validation/schemas.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/validation/schemas.ts). This central location prevents duplication and allows multiple routes to reuse validation logic for shared data structures.

### How do I protect a route so it only accepts authenticated requests?

Import `extractApiKey` and `isValidApiKey` from [`src/sse/services/auth.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/sse/services/auth.ts) inside your POST handler. Extract the key from the request headers, validate it, and throw an error before calling any core handler if validation fails.

### What happens if I don't use `buildErrorBody()` for errors?

Without `buildErrorBody()`, raw error objects—including stack traces and internal implementation details—may leak to the API client. This violates OmniRoute's security standards and exposes sensitive information about your infrastructure.

### Why does every route need an explicit OPTIONS handler?

Browsers send OPTIONS pre-flight requests for cross-origin checks before executing POST, PUT, or DELETE methods. The explicit handler ensures every route responds with consistent `CORS_HEADERS` defined in [`src/shared/utils/cors.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/utils/cors.ts), preventing CORS errors in client applications.