How to Manage Nested Routes in OmniRoute: Next.js App Router Implementation Guide

OmniRoute leverages Next.js 16 App Router's file-system routing to create nested API endpoints by mapping directory hierarchies to URL paths, using [param] for dynamic segments and [...param] for catch-all routes that handle complex identifiers like provider-prefixed model names.

Learning how to manage nested routes in OmniRoute is essential for developers working with this open-source API gateway. The project uses Next.js 16 file-system conventions to structure its REST API under src/app/api/v1/, where every folder represents a URL segment and special patterns handle dynamic routing.

Understanding OmniRoute's File-System Routing Structure

OmniRoute exposes every public API endpoint under src/app/api/v1/, where the directory hierarchy mirrors the final URL structure. The routing system recognizes three distinct folder naming patterns:

Pattern Meaning Example Path
route.ts Leaf file implementing one or more HTTP verbs src/app/api/v1/chat/completions/route.ts
[param] Single-segment dynamic parameter injected into params src/app/api/v1/registered-keys/[id]/route.ts
[...param] Catch-all capturing remaining URL segments including slashes src/app/api/v1/models/[...model]/route.ts

Static routes take precedence over dynamic ones, ensuring concrete folders like chat/completions match before dynamic segments are evaluated.

Dynamic Route Patterns in OmniRoute

Single Segment Parameters ([param])

Dynamic parameters capture individual URL segments as typed values. In src/app/api/v1/registered-keys/[id]/route.ts, the [id] folder extracts the identifier from the path and injects it into the handler's params promise as a string.

Catch-All Segments ([...param])

Catch-all routes capture everything remaining in the URL path. According to the OmniRoute source code, this pattern serves two critical purposes:

  • Provider-prefixed identifiers: The models endpoint at src/app/api/v1/models/[...model]/route.ts uses this to capture identifiers containing slashes (e.g., claude/sonnet-3) without URL encoding issues.
  • Global error handling: The src/app/api/v1/[...omnirouteCatchAll]/route.ts file acts as a fallback for unknown /v1/* paths, returning structured JSON errors with type: "not_found" instead of HTML 404 pages.

Implementing Nested Sub-Resources

Nested folders represent sub-resources, creating intuitive API hierarchies. The revocation endpoint demonstrates how OmniRoute handles deep nesting:

// src/app/api/v1/registered-keys/[id]/revoke/route.ts
import { NextResponse } from "next/server";
import { isAuthenticated } from "../../../_shared/auth";

export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
  if (!(await isAuthenticated(request))) {
    return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
  }
  const { id } = await params;
  // Database revocation logic here
  return NextResponse.json({ revoked: id, status: "success" });
}

When a client sends POST /api/v1/registered-keys/abc123/revoke, Next.js resolves the path to this nested directory, extracts abc123 as id, and executes the revocation logic.

Handling Special Identifiers with Catch-All Routes

When resource identifiers contain slashes—common in AI model names—OmniRoute uses catch-all segments to preserve the full identifier intact:

// src/app/api/v1/models/[...model]/route.ts
export async function GET(_: Request, { params }: { params: Promise<{ model: string[] }> }) {
  const { model } = await params;
  const fullId = decodeURIComponent(model.join("/")); // Reconstructs "claude/sonnet-3"
  return handleGetModelById(_, fullId, getUnifiedModelsResponse);
}

The model parameter arrives as an array of path segments, which the handler joins to reconstruct the original identifier before querying the database.

Cross-Cutting Concerns and Shared Utilities

Rather than duplicating logic across nested routes, OmniRoute imports shared utilities from src/app/api/v1/_shared/*:

  • handleCorsOptions: Standardized CORS preflight responses
  • isAuthenticated: JWT verification and session validation
  • Error shaping utilities: Consistent JSON error formatting used by Response.json across all endpoints

This architectural pattern keeps individual route files minimal while ensuring consistent security, rate limiting, and observability throughout the nested route hierarchy.

Summary

  • OmniRoute maps URL paths directly to the directory structure under src/app/api/v1/ using Next.js 16 App Router conventions.
  • Use [param] folders for single dynamic segments and [...param] for catch-all routes that capture multiple path segments.
  • The global catch-all at src/app/api/v1/[...omnirouteCatchAll]/route.ts ensures API clients receive JSON 404 errors instead of HTML pages.
  • Catch-all routes enable support for slash-containing identifiers like provider/model-name without URL encoding complexity.
  • Shared utilities in src/app/api/v1/_shared/* provide consistent authentication, CORS handling, and error formatting across all nested routes.

Frequently Asked Questions

What is the difference between [param] and [...param] in OmniRoute?

The [param] syntax captures a single URL segment as a string value, suitable for IDs like abc123. The [...param] catch-all syntax captures all remaining path segments as an array, enabling OmniRoute to handle identifiers containing slashes (such as claude/sonnet-3) or to create global fallback handlers for unmatched routes.

How does OmniRoute handle 404 errors for unknown API paths?

OmniRoute implements a global catch-all route at src/app/api/v1/[...omnirouteCatchAll]/route.ts that intercepts any unmatched /v1/* requests. Instead of allowing Next.js to serve an HTML 404 page, this handler returns a JSON response with type: "not_found", maintaining consistent JSON communication that API clients and SDKs expect.

Can I add custom sub-resources to existing OmniRoute endpoints?

Yes, you can extend OmniRoute by creating additional nested directories under src/app/api/v1/. For example, to add a validate sub-resource to registered keys, create src/app/api/v1/registered-keys/[id]/validate/route.ts and export the appropriate HTTP method handlers. The file-system routing automatically wires the new endpoint to POST /api/v1/registered-keys/{id}/validate without additional configuration.

Where does OmniRoute store shared logic used by multiple routes?

Shared utilities, including authentication checks (isAuthenticated), CORS handling (handleCorsOptions), and error formatting, reside in src/app/api/v1/_shared/. Route files import these helpers to maintain consistency and avoid code duplication while keeping individual route implementations focused on specific business logic.

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 →