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

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

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

// 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, this returns the required access-control headers:

// 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. The pattern checks for application/json and validates payload size before parsing:

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 inspects request bodies for malicious patterns:

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 processes all chat completion requests. Routes import this handler and call it with the validated request:

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 (lines 17-22):

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

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

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

// 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 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.
  • 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 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 file inside it. For example, 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 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, 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 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →