Aura Router

createRouter

The createRouter function is a declarative API for defining routers that group endpoints and expose type-safe HTTP method handlers. The router automatically matches incoming requests based on the HTTP method and URL pattern, dispatching them to the correct endpoint.

createRouter is designed for use inside existing backend runtimes such as Next.js, Nuxt.js, SvelteKit, Cloudflare Workers, Bun, Deno, and Node.js (via Web APIs). It is not currently intended for building a backend from scratch, although this capability is planned for the future.

import { createRouter, createEndpoint } from "@aura-stack/router"

const getUsers = createEndpoint("GET", "/users", async (ctx) => {
  return Response.json({ users: [] })
})

const router = createRouter([getUsers], {
  basePath: "/api/v1",
})

// Request to GET /api/v1/users
export const { GET } = router

Overview

This documentation provides a comprehensive guide to the createRouter API, covering everything from configuration options to route matching internals.


Key Concepts

  • createRouter provides full type inference for endpoints, parameters, middlewares, and returned HTTP handlers.
  • All router-level configuration options apply automatically to every endpoint, including global middlewares, base path, and error handling.
  • The router returns only the HTTP methods defined by the endpoints.
  • Route matching is implemented with a Trie data structure, ensuring excellent performance for large APIs.

Type Inference

Arguments and options for createRouter are fully inferred, eliminating the need to specify generics. Explicit types can still be imported if stricter definitions are preferred.

Types can be accessed from the main entry points / or /types.

Example: Inferred Handlers

import { createRouter, createEndpoint } from "@aura-stack/router"

const signIn = createEndpoint("GET", "/auth/signIn/:oauth", async (ctx) => {
  const { oauth } = ctx.params
  return Response.json({ oauth })
})

const credentials = createEndpoint("POST", "/auth/credentials", async (ctx) => {
  return Response.json({ token: "jwt-token" })
})

export const { GET, POST } = createRouter([signIn, credentials])

// Type error — DELETE is not defined by any endpoint.
export const { DELETE } = createRouter([signIn, credentials])

Example: Explicit RouterConfig Usage

import { createRouter, type RouterConfig } from "@aura-stack/router"

const routerConfig: RouterConfig = {
  basePath: "/api/",
  use: [],
  onError: undefined,
  context: {},
}

export const router = createRouter([], routerConfig)

API Reference

Parameters

The createRouter function accepts configuration parameters to customize behavior.

import type { RouteEndpoint, RouterConfig, GetHttpHandlers } from "@aura-stack/router/types"

type createRouter = <const Endpoints extends RouteEndpoint[]>(
  endpoints: Endpoints,
  config: RouterConfig
) => GetHttpHandlers<Endpoints>
ParameterTypeDescription
endpointsRouteEndpoint[]The list of endpoints created via createEndpoint.
configRouterConfigOptional router configuration.

Endpoints

import type { HTTPMethod, RoutePattern, EndpointConfig, RouteHandler } from "@aura-stack/router/types"

interface RouteEndpoint<
  Method extends HTTPMethod | HTTPMethod[] = HTTPMethod | HTTPMethod[],
  Route extends RoutePattern = RoutePattern,
  Config extends EndpointConfig = EndpointConfig,
> {
  method: Method
  route: Route
  handler: RouteHandler<Route, Config>
  config: Config
}

The endpoints parameter is a list of endpoint definitions to be loaded and managed by the router. All HTTP methods defined in the router are returned as HTTP route handlers and can be accessed based on specific needs (see Type Inference).

import { createRouter, createEndpoint } from "@aura-stack/router"

const signIn = createEndpoint("GET", "/auth/:oauth", async (ctx) => {
  const oauth = ctx.oauth
  return Response.json({ oauth })
})

const callback = createEndpoint("POST", "/callback/:oauth", async (ctx) => {
  const oauth = ctx.oauth
  return Response.json({ oauth }, { status: 301 })
})

export const { GET } = createRouter([signIn, callback])

RouterConfig

The config parameter allows setting additional options for customization.

import type { RoutePattern, GlobalMiddleware, RouterError } from "@aura-stack/router/types"

interface RouterConfig {
  basePath?: RoutePattern
  use?: GlobalMiddleware[]
  onError?: (error: Error | RouterError, request: Request) => Response | Promise<Response>
  context?: GlobalMiddlewareContext
}
OptionTypeDescription
basePathstringPrefix applied to all routes (recommended: start with /).
useGlobalMiddleware[]Global middlewares executed before endpoint handlers.
onError(error: Error | RouterError, request: Request) => Response | Promise<Response>Global error handler.
contextGlobalMiddlewareContextGlobal context for endpoints and middlewares.
basePath

The basePath option is a router configuration that prefixes all endpoint routes. This is useful for API versioning or namespacing. The value should start with a leading slash (e.g., /api/v1). When the router is created, each endpoint's route is joined with the basePath, so incoming requests must include the full path (i.e., basePath + route).

import type { RoutePattern } from "@aura-stack/router/types"

// Expected: `/${string}`
export type BasePath = RoutePattern

The basePath is prepended to every endpoint route upon router creation. Requests must include the full path (basePath + route) — for example, with basePath: "/api/v1" and an endpoint route /users, the request path is /api/v1/users.

import { createRouter, type RouterConfig } from "@aura-stack/router"

const routerConfig: RouterConfig = {
  /**
   * Prefix to be added to all endpoints defined in the router.
   */
  basePath: "/api/v1",
}

const getUsers = createEndpoint("GET", "/users", async (ctx) => {
  return Response.json({ users: [] })
})

const createUser = createEndpoint("POST", "/users", async (ctx) => {
  return Response.json({ id: "new-user" }, { status: 201 })
})

/* 
  Request to GET /api/v1/users
  Request to POST /api/v1/users
*/
export const { GET, POST } = createRouter([getUsers, createUser], routerConfig)
use

The use configuration property defines global middleware functions that run for every matched request. Global middlewares execute after route matching but before endpoint middlewares and the route handler.

Global middlewares access the request via the context property. The context must be configured on the router to be accessible for global middlewares and endpoint middlewares.

Global middlewares are ideal for cross-cutting concerns such as:

  • Authentication / Authorization
  • Logging and Auditing
  • Redirect Rules
  • Request Validation
  • Early Returns (skipping handler execution)
  • Request Mutation or Shared Context Injection

Register global middlewares by adding them to the middlewares array in the router configuration, typed as GlobalMiddleware.

export type GlobalMiddleware = (
  ctx: GlobalMiddlewareContext
) => Response | GlobalMiddlewareContext | Promise<Response | GlobalMiddlewareContext>

Redirecting:

Global middlewares can handle route-level logic such as redirects.

import { createRouter, type GlobalMiddleware } from "@aura-stack/router"

const globalMiddleware: GlobalMiddleware = async (ctx) => {
  const url = new URL(ctx.url)
  if (url.pathname === "/api/v1") {
    return Response.redirect("https://unstable.aura-stack.com", 302)
  }
  return ctx
}

export const router = createRouter([], {
  use: [globalMiddleware],
})

Auditing & Logging:

Middleware is frequently used for analytics, logging, and request metadata injection.

import { createRouter, type GlobalMiddleware } from "@aura-stack/router"

const auditMiddleware: GlobalMiddleware = async (ctx) => {
  const timestamp = new Date().toISOString()
  console.log(`[${timestamp}] ${ctx.request.method} ${ctx.request.url}`)

  /*
    Add request ID to all responses
  */
  ctx.request.headers.set("x-request-id", crypto.randomUUID())
  return ctx
}

export const router = createRouter([], {
  use: [auditMiddleware],
})

Authorization / Early Return:

Global middlewares run before the route handler, making them ideal for request validation or early rejection to avoid executing expensive endpoint logic.

import { createRouter, type GlobalMiddleware } from "@aura-stack/router"

const authorizationMiddleware: GlobalMiddleware = async (ctx) => {
  const headers = new Headers(ctx.request.headers)
  if (!headers.has("Authorization")) {
    return Response.json({ message: "Unauthorized" }, { status: 401 })
  }
  return ctx
}

export const router = createRouter([], {
  use: [authorizationMiddleware],
})
Middleware Execution Order

Middlewares execute in a specific sequence:

  1. Global middlewares (from createRouter config)
  2. Endpoint middlewares (from createEndpointConfig)
  3. Route handler (the main endpoint function)
import { createRouter, createEndpoint, createEndpointConfig } from "@aura-stack/router"

const endpointConfig = createEndpointConfig({
  use: [
    async (ctx) => {
      console.log("2. Endpoint middleware")
      return ctx
    },
  ],
})

const endpoint = createEndpoint(
  "GET",
  "/test",
  async (ctx) => {
    console.log("3. Route handler")
    return Response.json({ ok: true })
  },
  endpointConfig
)

export const router = createRouter([endpoint], {
  use: [
    async (ctx) => {
      console.log("1. Global middleware")
      return ctx
    },
  ],
})
onError

The onError function is a global error handler that runs whenever an error is thrown inside a route handler, an endpoint middleware, or a global middleware. It allows overriding the default error response emitted by the router and returning a fully customized HTTP response.

type OnErrorHandler = (error: unknown, request: Request) => Response | Promise<Response>

Use it to:

  • Format errors consistently across the API.
  • Map errors to HTTP status codes.
  • Differentiate between internal router errors and application errors.
  • Add logging, alerting, or error telemetry.
  • Prevent unexpected backend responses from leaking implementation details.
Identifying Router Errors

To detect whether the error originated from the router internals or from endpoint logic, use the helper isRouterError.

A RouterError provides:

  • message
  • statusCode
  • statusText

These properties facilitate returning structured JSON error responses.

import { createRouter, isRouterError, type RouterConfig } from "@aura-stack/router"

const onError: RouterConfig["onError"] = (error, request) => {
  if (isRouterError(error)) {
    const { message, statusText, statusCode } = error
    return Response.json(
      {
        error: statusText,
        error_description: message,
      },
      { status: statusCode }
    )
  }
  return Response.json({ message: "Internal Server Error" }, { status: 500 })
}

export const router = createRouter([], {
  onError,
})
Handling Errors Thrown by Endpoints

If an endpoint throws an error, the router passes it directly to the onError function.

import { createEndpoint } from "@aura-stack/router"

const session = createEndpoint("GET", "/session", async () => {
  throw new Error("Unexpected error in GET /session")
})

export const { GET } = createRouter([session], {
  onError(error) {
    return Response.json({ error: error.message }, { status: 500 })
  },
})
Default Error Structure

Internally, router errors use a structured format containing:

  • A message in the JSON body.
  • A relevant status code.
  • A statusText label.

For example, when no route matches, the default response is:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "message": "No route found for path: /users"
}

If customization of 404 responses is required, handle them in the server layer (e.g., Next.js route handlers, Workers fetch events, etc.)

context

The context option defines global state for the application, accessible by endpoints and middlewares.

export interface GlobalContext {}

interface GlobalMiddlewareContext {
  request: Request
  context: GlobalContext
}

For better type safety with the global context, create a types.d.ts file and augment the module by updating the GlobalContext type with custom context object types. For more details, see Module Augmentation.

types.d.ts
import type { GlobalContext } from "@aura-stack/router"

declare module "@aura-stack/router" {
  interface GlobalContext {
    state: string
    appInstance: {
      name: string
      port: string
    }
  }
}
@/index
import { createRouter } from "@aura-stack/router"

export const router = createRouter([], {
  context: {
    state: "state-value",
    appInstance: {
      name: "app-name",
      port: process.env.PORT!,
    },
  },
})

Returns

The createRouter function returns a typed object containing only the HTTP handlers defined by the endpoints in the router. This ensures that developers can only access valid handler methods and prevents runtime errors by leveraging TypeScript's static type system.

ReturnTypeDescription
http handlersGetHttpHandlers<Endpoints extends RouteEndpoint[]>Generates an object containing only the allowed HTTP methods defined in the endpoints.

This return type ensures full type safety. If an endpoint does not define a DELETE method, TypeScript prevents access to DELETE on the router.

HTTP Route Handlers

The router returns only the HTTP handlers corresponding to the HTTP methods defined inside the configured endpoints.

This provides two core benefits:

  1. Type-Safe Access to Handlers: If the router doesn’t define a DELETE route, TypeScript will not allow router.DELETE.
  2. Zero Undefined Handlers at Runtime: The router guarantees that all returned handlers exist and are functional—preventing accidental undefined calls.
import { createRouter, createEndpoint } from "@aura-stack/router"
import type { RouterConfig, RoutePattern, GlobalMiddleware } from "@aura-stack/router/types"

const signIn = createEndpoint("GET", "/auth/signIn/:oauth", async (ctx) => {
  const { oauth } = ctx.params
  return Response.json({ oauth })
})

const credentials = createEndpoint("POST", "/auth/credentials", async (ctx) => {
  return Response.json({ token: "jwt-token" })
})

export const { GET, POST } = createRouter([signIn, credentials])

// Type Error @errors: 2339
export const { DELETE } = createRouter([signIn, credentials])

How It Works Internally

createRouter analyzes all endpoints and extracts their associated HTTP methods:

  • If at least one endpoint uses "GET" → the returned object includes GET.
  • If at least one endpoint uses "POST" → the returned object includes POST.
  • If no endpoint uses "PUT", "PATCH", etc. → they are omitted entirely.

Usage

Basic Usage

import { createRouter, createEndpoint } from "@aura-stack/router"

const getUsers = createEndpoint("GET", "/users", async (ctx) => {
  return Response.json({ users: [] })
})

const createUser = createEndpoint("POST", "/users", async (ctx) => {
  return Response.json({ id: "new-user" }, { status: 201 })
})

const router = createRouter([getUsers, createUser])

// Use the router handlers
const { GET, POST } = router

With basePath

import { createRouter, createEndpoint, type RouterConfig } from "@aura-stack/router"

const getUsers = createEndpoint("GET", "/users", async (ctx) => {
  return Response.json({ users: [] })
})

const createUser = createEndpoint("POST", "/users", async (ctx) => {
  return Response.json({ id: "new-user" }, { status: 201 })
})

const config: RouterConfig = {
  basePath: "/v1",
}

export const { GET, POST } = createRouter([getUsers, createUser], config)

With use

import { createRouter, createEndpoint, type RouterConfig, type GlobalMiddleware } from "@aura-stack/router"

const getUsers = createEndpoint("GET", "/users", async (ctx) => {
  return Response.json({ users: [] })
})

const createUser = createEndpoint("POST", "/users", async (ctx) => {
  return Response.json({ id: "new-user" }, { status: 201 })
})

const auditMiddleware: GlobalMiddleware = async (ctx) => {
  const timestamp = new Date().toISOString()
  console.log(`[${timestamp}] ${ctx.request.method} ${ctx.request.url}`)

  ctx.request.headers.set("x-request-id", crypto.randomUUID())
  return ctx
}

const config: RouterConfig = {
  use: [auditMiddleware],
}

export const { GET, POST } = createRouter([getUsers, createUser], config)

With onError

import { createRouter, createEndpoint, type RouterConfig } from "@aura-stack/router"

const getUsers = createEndpoint("GET", "/users", async (ctx) => {
  return Response.json({ users: [] })
})

const createUser = createEndpoint("POST", "/users", async (ctx) => {
  return Response.json({ id: "new-user" }, { status: 201 })
})

const onError: RouterConfig["onError"] = (error, request) => {
  if (isRouterError(error)) {
    const { message, statusText, statusCode } = error
    return Response.json(
      {
        error: statusText,
        error_description: message,
      },
      { status: statusCode }
    )
  }
  return Response.json({ message: "Internal Server Error" }, { status: 500 })
}

export const { GET, POST } = createRouter([getUsers, createUser], {
  onError,
})

With context

Augment the module:

types.d.ts
import type { GlobalContext } from "@aura-stack/router"

declare module "@aura-stack/router" {
  interface GlobalContext {
    state: boolean
    appInstance: {
      name: string
      port: string
    }
  }
}

Define the router:

import { createRouter, createEndpoint, type RouterConfig } from "@aura-stack/router"

const getUser = createEndpoint("GET", "/users/:id", async (ctx) => {
  const userId = ctx.params.id
  return Response.json({ userId })
})

const createUser = createEndpoint("POST", "/users", async (ctx) => {
  const userData = await ctx.request.json()
  return Response.json({ id: "new-user", ...userData }, { status: 201 })
})

const config: RouterConfig = {
  context: {
    state: true,
    appInstance: {
      name: "api-service",
      port: "3000",
    },
  },
}

export const { GET, POST } = createRouter([getUser, createUser], config)

Route Matching

Aura Router uses a Trie-based route matcher to achieve fast and predictable routing, even across large endpoint trees.

This design provides:

  • ⚡ O(K) lookup time (K = number of path segments).
  • 🧠 Deterministic precedence rules.
  • 🔒 Strict parameter name validation.
  • 🎯 Zero ambiguity between static and dynamic routes.

The router is TypeScript-first, so the Trie is optimized for both runtime performance and compile-time inference.

Route Matching Priority

The matching process follows a strict hierarchy to avoid ambiguity.

PriorityType of RouteDescriptionCases
1 (highest)Static routesAlways matched before dynamic routes./me, /users/, etc.
2Dynamic routesMatched only if no static route applies./:id, :userId, etc.
/*
 Specific routes before dynamic ones
*/
export const { GET } = createRouter([
  createEndpoint("GET", "/users/me", () => Response.json({ message: "User me" })),
  createEndpoint("GET", "/users/:id", () => Response.json({ message: "User by id" })),
])

Each dynamic segment must use a consistent parameter name for a given path level. Otherwise, the router throws a configuration error.

Valid:

/users/:userId
/users/:userId/books

Invalid:

/users/:userId
/users/:id

Parameter name mismatches break type inference and cause ambiguous routing. Aura Router enforces this to guarantee consistent ctx.params.

Mini Example of the Internal Trie

For this set of routes:

/users/me
/users/:id

The Trie becomes:

root
 └── "users"
       ├── "me"        (static)
       └── :id         (dynamic)

On this page