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 } = routerOverview
This documentation provides a comprehensive guide to the createRouter API, covering everything from configuration options to route matching internals.
Key Concepts
createRouterprovides 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>| Parameter | Type | Description |
|---|---|---|
endpoints | RouteEndpoint[] | The list of endpoints created via createEndpoint. |
config | RouterConfig | Optional 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
}| Option | Type | Description |
|---|---|---|
basePath | string | Prefix applied to all routes (recommended: start with /). |
use | GlobalMiddleware[] | Global middlewares executed before endpoint handlers. |
onError | (error: Error | RouterError, request: Request) => Response | Promise<Response> | Global error handler. |
context | GlobalMiddlewareContext | Global 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 = RoutePatternThe 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:
- Global middlewares (from
createRouterconfig) - Endpoint middlewares (from
createEndpointConfig) - 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:
messagestatusCodestatusText
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
messagein the JSON body. - A relevant
statuscode. - A
statusTextlabel.
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.
import type { GlobalContext } from "@aura-stack/router"
declare module "@aura-stack/router" {
interface GlobalContext {
state: string
appInstance: {
name: string
port: string
}
}
}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.
| Return | Type | Description |
|---|---|---|
| http handlers | GetHttpHandlers<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:
- Type-Safe Access to Handlers: If the router doesn’t define a
DELETEroute, TypeScript will not allowrouter.DELETE. - 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 } = routerWith 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:
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.
| Priority | Type of Route | Description | Cases |
|---|---|---|---|
| 1 (highest) | Static routes | Always matched before dynamic routes. | /me, /users/, etc. |
| 2 | Dynamic routes | Matched 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/booksInvalid:
/users/:userId
/users/:idParameter 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/:idThe Trie becomes:
root
└── "users"
├── "me" (static)
└── :id (dynamic)