How to Add a New API Route to OmniRoute Following Its Standard Pattern
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, optionally authenticate requests via helpers in 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 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, and you can observe the implementation in the existing entry point at 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. This handles browser pre-flight requests before the actual method calls.
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 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. This central location prevents schema duplication and guarantees type-safety throughout the application.
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. Use extractApiKey() to parse the header and isValidApiKey() to verify credentials.
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 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.
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 and wrap your entire handler logic in a try-catch block.
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 following this full skeleton:
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:
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 to verify validation and error handling:
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 and docs/openapi.yaml to expose the new endpoint to external consumers.
Summary
- CORS handling: Every
route.tsmust export anOPTIONShandler returningCORS_HEADERSfromsrc/shared/utils/cors.ts. - Input validation: Centralize Zod schemas in
src/shared/validation/schemas.tsto enforce type-safety and protect against malformed payloads. - Authentication: Use
extractApiKey()andisValidApiKey()fromsrc/sse/services/auth.tsfor 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()fromopen-sse/utils/error.tsto prevent stack trace exposure. - Documentation: Update
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. 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 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, preventing CORS errors in client applications.
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 →