Middleware and request context
Put cross-cutting request behavior in an ordered chain and expose only the typed context values that downstream middleware and handlers need.
Rouzer includes request context and middleware composition in its root API, so most applications can import route, middleware, and adapter helpers from one package.
import {
chain,
filterRuntime,
type Middleware,
type MiddlewareContext,
type RequestContext,
type RequestHandler,
} from 'rouzer'
Middleware Chain
chain() creates a MiddlewareChain. Rouzer routers are middleware chains too,
so you can append middleware before attaching routes.
const requestInfo = chain().use(ctx => ({
requestId: ctx.request.headers.get('x-request-id') ?? crypto.randomUUID(),
}))
const router = createRouter()
.use(requestInfo)
.use(routes, {
getProfile(ctx) {
return loadProfile(ctx.path.id, ctx.requestId)
},
})
Middleware runs in order. A middleware can:
- return
voidorundefinedto continue without adding context - return a
Responseto stop the chain and send that response - return a request plugin object whose properties are merged into the downstream context
- call
ctx.passThrough()to skip the rest of the current chain
Request Context
Every middleware and Rouzer handler receives a RequestContext.
| Property | Purpose |
|---|---|
ctx.request |
The Web Request. |
ctx.url |
Lazily parsed URL for ctx.request.url. |
ctx.host |
Host data such as ip, runtime, env, and waitUntil. |
ctx.env(name) |
Typed environment lookup backed by ctx.host.env and middleware env plugins. |
ctx.waitUntil(promise) |
Delegates background work to ctx.host.waitUntil when available. |
ctx.setHeader(name, value) |
Sets a response header from request middleware. |
ctx.onResponse(callback) |
Registers a callback that can mutate or replace the final response. |
ctx.passThrough() |
Chain-local control flow for unresolved requests. |
Host-provided values live under ctx.host.
const runtimeName = ctx.host.runtime?.name
const ip = ctx.host.ip
Runtime-specific request data should stay behind ctx.host.runtime.
Adding Context Properties
Return a plain object to make properties available to downstream middleware and handlers.
const sessionMiddleware = chain().use(async ctx => {
const token = ctx.request.headers.get('authorization')
const session = token ? await readSession(token) : null
if (!session) {
return new Response('Unauthorized', { status: 401 })
}
return { session }
})
Attach the middleware before route handlers that need the property.
createRouter()
.use(sessionMiddleware)
.use(routes, {
me(ctx) {
return ctx.session.user
},
})
Treat context property names as owned by the middleware that introduces them. Avoid collisions between unrelated middleware.
Environment Bindings
Return an env object to add typed variables behind ctx.env(...).
const envMiddleware = chain().use(() => ({
env: {
DATABASE_URL: process.env.DATABASE_URL,
},
}))
const router = createRouter()
.use(envMiddleware)
.use(routes, {
health(ctx) {
return { configured: Boolean(ctx.env('DATABASE_URL')) }
},
})
Important
The env key is reserved for environment bindings. Read its values through
ctx.env(name), not as ctx.env.DATABASE_URL.
Runtime Type Markers
Important
The runtime request plugin key is a type-level marker for
ctx.host.runtime; it does not add ctx.runtime.
const nodeOnly = chain()
.use(filterRuntime<{ name: 'node' }>('node'))
.use(ctx => {
ctx.host.runtime?.name
})
filterRuntime(name) checks ctx.host.runtime?.name. If the runtime name does
not match, it calls ctx.passThrough().
Response Callbacks
Use ctx.onResponse(callback) or return { onResponse } when middleware needs
to finalize a response after a route handler runs.
const timing = chain().use(ctx => {
const startedAt = Date.now()
ctx.onResponse(response => {
response.headers.set('server-timing', `app;dur=${Date.now() - startedAt}`)
})
})
Use ctx.setHeader(name, value) from request middleware for simple headers.
Inside response callbacks, mutate response.headers directly or return a new
Response.
Isolation
Use .isolate() when you want to run a chain for side effects or early
responses without leaking its plugin properties into the parent chain.
const optionalAuth = chain()
.use(readOptionalSession)
.use(auditSession)
.isolate()
const router = createRouter().use(optionalAuth).use(routes, handlers)
An isolated chain can still return a Response. Its context additions stay
inside the isolated chain.
Pass Through
Note
ctx.passThrough() is chain-local control flow, not an adapter pass-through.
In a nested or isolated chain, it skips the rest of that chain and lets the
parent chain continue. In a final fetch handler, an unresolved request becomes
the default 404 Not Found response.
Use passThrough for optional middleware branches or runtime filters. Use a
Response when you want to deliberately stop the request.