How to Group Routes in OmniRoute: Next.js App Router Organization Guide

OmniRoute groups API routes by leveraging Next.js App Router's file-system convention, where nested folders under src/app/api/v1/ automatically create logical URL namespaces like /v1/chat or /v1/tts.

OmniRoute is an open-source API gateway that organizes its public HTTP endpoints using the Next.js App Router's folder-based routing system. Learning how to group routes in OmniRoute enables you to structure endpoints by feature—such as chat, embeddings, or images—while maintaining a clean, scalable codebase. All route definitions reside under src/app/api/v1/, with the directory hierarchy directly mapping to the exposed URL structure.

Understanding the Route Group Hierarchy

OmniRoute implements a four-level nesting strategy to separate concerns and manage complexity. According to the OmniRoute source code, the hierarchy works as follows:

Root API Entry Point

The foundation sits at src/app/api/v1/route.ts, which acts as the top-level catch-all that forwards requests to sub-folders. This file handles the initial routing logic for all version 1 endpoints.

Feature Groups

Logical collections group related endpoints by functionality. For example, src/app/api/v1/chat/completions/route.ts contains all chat-completion handlers, while src/app/api/v1/images/ handles image generation. The folder name becomes the URL prefix automatically.

Dynamic Segments

Parameterized paths accept variables like API keys or tokens within the URL. The path src/app/api/v1/vscode/[token]/v1/chat/completions/route.ts demonstrates how dynamic segments (denoted by square brackets) allow VS Code-specific routes to extract a token directly from the request path.

Shared Utilities

Common middleware and helpers live in src/app/api/v1/_shared/ and src/app/api/v1/_helpers/. Files like src/app/api/v1/_shared/rateLimit.ts provide reusable rate-limiting logic imported by multiple route groups.

How to Create a New Route Group

To add a new functional group to OmniRoute, follow this workflow:

  1. Create a folder under src/app/api/v1/ that reflects the feature name (e.g., tts for text-to-speech).

  2. Add a route.ts file that exports the HTTP handlers (GET, POST, etc.). Use [[...slug]].ts for catch-all routes if needed.

  3. Reuse shared helpers by importing utilities from src/app/api/v1/_shared/ for CORS, Zod validation, authentication, or rate limiting.

  4. Optionally nest sub-folders for further granularity (e.g., v1/models/, v1/embeddings/).

Standard Route Implementation Pattern

All routes in OmniRoute follow a consistent pattern that applies middleware before delegating to core handlers. In src/app/api/v1/chat/completions/route.ts, the implementation chains CORS, authentication, and Zod validation:

// src/app/api/v1/chat/completions/route.ts
import { handleChatCore } from '@/open-sse/handlers/chatCore';
import { z } from 'zod';
import { cors } from '@/open-sse/middleware/cors';
import { auth } from '@/open-sse/middleware/auth';

export async function POST(req: Request) {
  // Apply CORS & auth middleware
  await cors(req);
  await auth(req);

  // Validate body with Zod
  const schema = z.object({
    model: z.string(),
    messages: z.array(z.object({ role: z.string(), content: z.string() })),
    // … other fields …
  });
  const body = await req.json();
  schema.parse(body);

  // Delegate to the core handler
  return handleChatCore(body, req);
}

The router automatically exposes any files under src/app/api/v1/chat/* beneath the /v1/chat URL namespace.

Practical Grouping Examples

Adding a Text-to-Speech Group

To create a new "text-to-speech" endpoint group, execute the following commands to set up the folder and route file:

mkdir -p src/app/api/v1/tts
cat > src/app/api/v1/tts/route.ts <<'EOF'
import { handleTts } from '@/open-sse/handlers/tts';
import { cors } from '@/open-sse/middleware/cors';
import { auth } from '@/open-sse/middleware/auth';
import { z } from 'zod';

export async function POST(req: Request) {
  await cors(req);
  await auth(req);

  const schema = z.object({
    model: z.string(),
    input: z.string(),
  });
  const body = await req.json();
  schema.parse(body);

  return handleTts(body, req);
}
EOF

Once created, the endpoint is immediately available at:

POST https://localhost:20128/v1/tts
{
  "model": "elevenlabs/tts",
  "input": "Hello, OmniRoute!"
}

Grouping with Shared Middleware

Existing features can import shared utilities to maintain consistency. In src/app/api/v1/images/edits/route.ts, the route applies centralized rate limiting before processing:

// src/app/api/v1/images/edits/route.ts
import { handleImageEdit } from '@/open-sse/handlers/imageEdit';
import { rateLimit } from '@/app/api/v1/_shared/rateLimit';

export async function POST(req: Request) {
  await rateLimit(req);
  return handleImageEdit(req);
}

Key Files for Route Organization

Understanding these core files helps navigate the OmniRoute codebase:

Summary

  • OmniRoute uses Next.js App Router file-system routing to organize API endpoints under src/app/api/v1/.
  • Folder names automatically create URL namespaces—any folder created under v1/ becomes a route prefix.
  • Shared middleware (CORS, auth, rate limiting) lives in src/app/api/v1/_shared/ and is imported by specific route handlers.
  • Dynamic segments use square-bracket notation (e.g., [token]) to capture URL parameters.
  • New groups require only a folder and a route.ts file exporting HTTP method handlers.

Frequently Asked Questions

How does OmniRoute automatically group routes?

OmniRoute leverages Next.js App Router conventions where the directory structure under src/app/api/v1/ directly maps to URL paths. Any folder created inside v1/ automatically groups its contained route.ts files under that folder's name in the URL (e.g., v1/chat/ becomes /v1/chat/*).

Can I use dynamic parameters in route groups?

Yes. OmniRoute supports dynamic segments using square-bracket folder names like [token]. The file src/app/api/v1/vscode/[token]/v1/chat/completions/route.ts captures the token value from the URL and makes it available to the route handler for scoping requests or authentication.

Where should I place middleware that applies to multiple groups?

Reusable middleware should be placed in src/app/api/v1/_shared/ (for utilities like rateLimit.ts) or src/app/api/v1/_helpers/ (for API key scoping logic). Route files then import and invoke these functions explicitly, as seen in src/app/api/v1/chat/completions/route.ts with cors(req) and auth(req).

What is the difference between _shared and _helpers folders?

In OmniRoute's structure, src/app/api/v1/_shared/ typically contains middleware and infrastructure code like CORS headers and rate limiting. The src/app/api/v1/_helpers/ directory contains business-logic utilities such as apiKeyScope.ts that manage API key validation and scoping across different route groups.

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 →