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 } = routerWhat 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
createRouterprovides 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>| Parameter | Type | Description |
|---|---|---|
endpoints | RouteEndpoint[] | The list of endpoints created via createEndpoint |
config | RouterConfig | Optional 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
}| Option | Type | Description |
|---|---|---|
basePath | string | Prefix applied to all routes (recommended: start with /) |
middlewares | 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 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 = RoutePatternThe 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:
- 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({
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
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 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.
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 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:
- Type-Safe Access to Handlers: If your router doesn’t define a
DELETEroute, TypeScript will not allow router.DELETE. - 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 } = 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 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
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.
| 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 param name for a given path level. Otherwise, the router throws a configuration error.
Valid
/users/:userId
/users/:userId/booksInvalid
/users/:userId
/users/:idParam 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)