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:
src/app/api/v1/chat/completions/route.ts→POST /v1/chat/completionssrc/app/api/v1/models/route.ts→GET /v1/modelssrc/app/api/v1/custom/hello/route.ts→GET /v1/custom/hello
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.tsfiles undersrc/app/api/v1/<path>/to create endpoints at/v1/<path>. - Export HTTP verb functions: Name exports
GET,POST, orOPTIONSto handle specific methods; each receives aRequestand returns aResponse. - 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
createInjectionGuardfrom@/middleware/promptInjectionGuardto screen requests for injection attacks. - Delegate AI logic: Route to
handleChatinsrc/sse/handlers/chat.tsfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →