Aura Router

createRouter

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

createRouter is designed to be used inside existing backend runtimes such as Next.js, Nuxt.js, SvelteKit, Cloudflare Workers, Bun, Deno, and Node.js (via Web APIs). It is not yet intended to build 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

What you'll learn

Through this api reference documentation you are going to learn and understand from basic to advanced about the createRouter API Reference:


Good to know

  • createRouter provides full type inference for endpoints, parameters, middlewares, and returned HTTP handlers.
  • All router-level configuration applies automatically to every endpoint:
    • global middlewares
    • base path
    • error handling
  • The router returns only the HTTP methods used in your endpoints.
  • Route matching is implemented with a Trie, providing excellent performance for large APIs.

Type Inference

Arguments and options for createRouter are fully inferred without needing to specify generics. You may still import the types if you prefer explicit definitions.

The types can be accessed from the main entry points / or /types. Available types:

  • RouterConfig.
  • RoutePattern.
  • GlobalMiddleware.
  • GlobalMiddlewareContext.
  • RouterConfig["onError"].

Example of 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])

Explicit RouterConfig usage:

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

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

export const router = createRouter([], routerConfig)

API Reference

Parameters

Set of parameters accepted by the router to configure it.

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 RouteEndpoint[]

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

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

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 your 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 RouterConfig

For router configuration you can set extra options for a better customization and definitions based on your needs.

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

interface RouterConfig {
  basePath?: RoutePattern
  middlewares?: GlobalMiddleware[]
  onError?: (error: Error | RouterError, request: Request) => Response | Promise<Response>
  context?: GlobalMiddlewareContext
}
OptionTypeDescription
basePathstringPrefix applied to all routes (recommended: start with /)
middlewaresGlobalMiddleware[]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 RoutePattern

basePath is an optional router configuration that prefixes all endpoint routes. This is useful for API versioning or namespacing. The value should start with a leading slash (for example: /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 when the router is created. Provide a value that begins with a leading slash (for example: /api or /api/v1) so the router can join it with each endpoint route. 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)

Example with different base paths:

/* 
  Public API
*/
const publicRouter = createRouter([...publicEndpoints], {
  basePath: "/api/v1/public",
})

/*
 Admin API
*/
const adminRouter = createRouter([...adminEndpoints], {
  basePath: "/api/v1/admin",
})

/*
 Internal API
*/
const internalRouter = createRouter([...internalEndpoints], {
  basePath: "/api/v1/internal",
})
middlewares GlobalMiddleware[]

middlewares is an optional router config property used to register global middleware functions that run for every matched request. Global middlewares execute after route matching but before endpoint middlewares and the route handler. Add them to the router configuration as an array (middlewares) and type them as GlobalMiddleware.

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 logic that should apply across all endpoints, such as:

  • Authentication / authorization
  • Logging and auditing
  • Redirect rules
  • Request validation
  • Early returns to skip handler execution
  • Mutating the request or shared context

You 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([], {
  middlewares: [globalMiddleware],
})
  • Auditing & Logging

Middleware is often 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([], {
  middlewares: [auditMiddleware],
})
  • Authorization / Early Return

Global middlewares run before the router handler, so they are 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([], {
  middlewares: [authorizationMiddleware],
})
Middleware Execution Order

Middlewares execute in a specific order:

  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({
  middlewares: [
    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], {
  middlewares: [
    async (ctx) => {
      console.log("1. Global middleware")
      return ctx
    },
  ],
})
onError RouterConfig["onError"]

onError 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 you to override the default error response emitted by the router and return a fully customized HTTP response.

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

Use it to:

  • Format errors consistently across your 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 your own endpoint logic, use the helper isRouterError.

A RouterError provides:

  • message.
  • statusCode.
  • statusText.

These make it straightforward to return 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 will pass it directly to your 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 you need to customize 404 responses, you may handle them in your server layer (e.g., Next.js route handlers, Workers fetch events, etc.)

context GlobalMiddlewareContext

context is an optional router configuration which its defined global states to the application to be acceded 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 your 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 your endpoints.

This return type ensures full type-safety. If an endpoint does not define a DELETE method, TypeScript will prevent you from accessing DELETE on the router.

HTTP Route Handlers

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

This has two core benefits:

  1. Type-Safe Access to Handlers: If your 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—no 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 middlewares

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 = {
  middlewares: [auditMiddleware],
}

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

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 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 param-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 param name for a given path level. Otherwise, the router throws a configuration error.

Valid

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

Invalid

/users/:userId
/users/:id

Param 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)