# How to Define Routes in OmniRoute: A Complete Guide to File-Based API Routing

> Learn to define routes in OmniRoute using its file-based API routing. Our guide explains how to expose TypeScript files as HTTP endpoints with automatic URL mapping.

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

---

**OmniRoute uses Next.js App Router to automatically expose TypeScript files in `src/app/api/v1/` as HTTP endpoints, where the folder path determines the URL and exported functions named `GET`, `POST`, or `OPTIONS` handle the corresponding HTTP methods.**

OmniRoute is an open-source AI gateway built on Next.js that defines API routes using a file-system convention. Understanding how to define routes in OmniRoute requires familiarity with its `src/app/api/v1/` directory structure, where each [`route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/route.ts) file becomes a live endpoint. This guide covers the exact file locations, method signatures, and security patterns used in the diegosouzapw/OmniRoute repository.

## File-Based Routing Fundamentals

OmniRoute leverages the Next.js App Router convention where the URL path is derived directly from the folder hierarchy. Every route lives under `src/app/api/v1/` and follows a strict mapping between file location and API endpoint.

**Route-to-File Mapping:**
- [`src/app/api/v1/chat/completions/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/chat/completions/route.ts) → `POST /v1/chat/completions`
- [`src/app/api/v1/models/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/models/route.ts) → `GET /v1/models`
- [`src/app/api/v1/custom/hello/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/custom/hello/route.ts) → `GET /v1/custom/hello`

Each [`route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/route.ts) file must export async functions named after the HTTP verbs they handle. The function receives a standard Web `Request` object and must return a `Response` object.

## Required Route Structure

A valid OmniRoute endpoint requires three core components: HTTP method exports, CORS pre-flight handling, and proper TypeScript signatures.

### HTTP Method Exports

Define handlers by exporting functions named exactly after the HTTP method:

```typescript
// src/app/api/v1/example/route.ts
export async function GET(request: Request) {
  return new Response(JSON.stringify({ status: "ok" }), {
    status: 200,
    headers: { "Content-Type": "application/json" }
  });
}

export async function POST(request: Request) {
  const body = await request.json();
  // Process request...
  return new Response(JSON.stringify({ result: body }), { status: 200 });
}

```

### CORS Pre-Flight Handling

Every public route must implement an `OPTIONS` handler to handle browser pre-flight requests. According to the source in [`src/app/api/v1/models/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/models/route.ts), this returns the required access-control headers:

```typescript
// src/app/api/v1/models/route.ts (lines 6-13)
export async function OPTIONS() {
  return new Response(null, {
    headers: {
      "Access-Control-Allow-Methods": "GET, OPTIONS",
      "Access-Control-Allow-Headers": "*",
      "Access-Control-Allow-Origin": "*"
    }
  });
}

```

## Security and Validation Patterns

OmniRoute routes implement defense-in-depth by validating requests before reaching the core handler logic.

### Content-Type and JSON Validation

The chat completions route demonstrates strict content-type guarding at lines 47-60 in [`src/app/api/v1/chat/completions/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/chat/completions/route.ts). The pattern checks for `application/json` and validates payload size before parsing:

```typescript
const contentType = request.headers.get("content-type");
if (!contentType?.includes("application/json")) {
  return new Response(
    JSON.stringify({ error: "Content-Type must be application/json" }),
    { status: 415, headers: { "Content-Type": "application/json" } }
  );
}

```

### Prompt Injection Protection

Security middleware is integrated via shared utilities. The injection guard in [`src/middleware/promptInjectionGuard.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/middleware/promptInjectionGuard.ts) inspects request bodies for malicious patterns:

```typescript
import { createInjectionGuard } from "@/middleware/promptInjectionGuard";

const injectionGuard = createInjectionGuard();

export async function POST(request: Request) {
  const body = await request.clone().json().catch(() => null);
  if (body) {
    const { blocked, result } = injectionGuard(body);
    if (blocked) {
      return new Response(
        JSON.stringify({ error: "Prompt injection detected", details: result }),
        { status: 400, headers: { "Content-Type": "application/json" } }
      );
    }
  }
  // Proceed to handler...
}

```

## Handler Delegation and Response Types

Most AI routes delegate provider-specific work to unified handlers rather than implementing logic directly in the route file.

### Delegation to Unified Handlers

The `handleChat` function in [`src/sse/handlers/chat.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/sse/handlers/chat.ts) processes all chat completion requests. Routes import this handler and call it with the validated request:

```typescript
import { handleChat } from "@/sse/handlers/chat";

export async function POST(request: Request) {
  // Validation and security checks...
  return handleChat(request);
}

```

### Streaming vs. JSON Response Logic

Routes determine whether to stream Server-Sent Events (SSE) or return plain JSON based on the `stream` flag or `Accept` header. As implemented in [`src/app/api/v1/chat/completions/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/chat/completions/route.ts) (lines 17-22):

```typescript
const acceptsStream = request.headers.get("Accept")?.includes("text/event-stream");
const shouldStream = body.stream || acceptsStream;

if (shouldStream) {
  return new Response(streamingResponse, {
    headers: { "Content-Type": "text/event-stream" }
  });
}

```

## Complete Route Examples

### Minimal Hello World Route

```typescript
// src/app/api/v1/hello/route.ts
import { CORS_HEADERS } from "@/shared/utils/cors";

export async function OPTIONS() {
  return new Response(null, {
    headers: { "Access-Control-Allow-Methods": "GET, OPTIONS", ...CORS_HEADERS }
  });
}

export async function GET(_request: Request) {
  return new Response(
    JSON.stringify({ message: "Hello from OmniRoute" }),
    { status: 200, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }
  );
}

```

### Production Chat Route with Initialization

```typescript
// src/app/api/v1/custom/chat/route.ts
import { CORS_HEADERS } from "@/shared/utils/cors";
import { handleChat } from "@/sse/handlers/chat";
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
import { createInjectionGuard } from "@/middleware/promptInjectionGuard";

let initPromise = null;
const injectionGuard = createInjectionGuard();

function ensureInitialized() {
  if (!initPromise) {
    initPromise = Promise.resolve(initTranslators()).then(() => {
      console.log("[SSE] Translators ready");
    });
  }
  return initPromise;
}

export async function POST(request: Request) {
  await ensureInitialized();
  
  const body = await request.clone().json().catch(() => null);
  if (body) {
    const { blocked } = injectionGuard(body);
    if (blocked) {
      return new Response(
        JSON.stringify({ error: "Prompt injection detected" }),
        { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }
      );
    }
  }
  
  return handleChat(request);
}

export async function OPTIONS() {
  return new Response(null, {
    headers: { "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "*", ...CORS_HEADERS }
  });
}

```

### Custom Health Check Route

```typescript
// src/app/api/v1/health/route.ts
import { CORS_HEADERS } from "@/shared/utils/cors";
import { getHealthStatus } from "@/services/health";

export async function GET(_request: Request) {
  const status = await getHealthStatus();
  return new Response(JSON.stringify(status), {
    status: 200,
    headers: { ...CORS_HEADERS, "Content-Type": "application/json" }
  });
}

export async function OPTIONS() {
  return new Response(null, {
    headers: { "Access-Control-Allow-Methods": "GET, OPTIONS", ...CORS_HEADERS }
  });
}

```

## Summary

- **File location determines the URL**: Place [`route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/route.ts) files under `src/app/api/v1/<path>/` to create endpoints at `/v1/<path>`.
- **Export HTTP verb functions**: Name exports `GET`, `POST`, or `OPTIONS` to handle specific methods; each receives a `Request` and returns a `Response`.
- **Always include OPTIONS**: Implement CORS pre-flight handling using shared utilities from [`src/shared/utils/cors.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/utils/cors.ts).
- **Validate before processing**: Check content-type headers and JSON payloads before delegation to prevent malformed requests.
- **Use security middleware**: Import `createInjectionGuard` from `@/middleware/promptInjectionGuard` to screen requests for injection attacks.
- **Delegate AI logic**: Route to `handleChat` in [`src/sse/handlers/chat.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/sse/handlers/chat.ts) for chat completions, or implement custom logic for specialized endpoints.

## Frequently Asked Questions

### Where should I place new route files in OmniRoute?

Create a folder structure under `src/app/api/v1/` that mirrors your desired URL path, then add a [`route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/route.ts) file inside it. For example, [`src/app/api/v1/services/translate/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/services/translate/route.ts) automatically becomes accessible at `POST /v1/services/translate`. The folder hierarchy defines the route segments, and the [`route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/route.ts) file contains the exported HTTP method handlers.

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

The OPTIONS handler responds to CORS pre-flight requests required by browsers before making cross-origin API calls. According to the implementation in [`src/app/api/v1/models/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/models/route.ts), this function returns the necessary `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` headers to permit web clients to communicate with the API.

### How do I add prompt injection protection to a custom route?

Import `createInjectionGuard` from `@/middleware/promptInjectionGuard` and instantiate it at the module level. In your POST handler, clone the request and parse the JSON body, then pass it to the guard's returned function. If the guard returns `blocked: true`, return a 400 response immediately before processing the request further, as demonstrated in [`src/app/api/v1/chat/completions/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/v1/chat/completions/route.ts) lines 84-107.

### Can I create routes that bypass the standard `handleChat` handler?

Yes. While most AI routes delegate to `handleChat` in [`src/sse/handlers/chat.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/sse/handlers/chat.ts), you can implement custom logic directly in the route file for health checks, provider-specific endpoints, or monitoring. Simply do not import `handleChat` and return your own Response object after processing the request according to your custom requirements.