Guides
Writing Middleware
Middleware should make one clear request-phase decision: continue, extend the context, or return a response.
Return Shapes
Middleware receives a RequestContext and may return one of three meaningful
values.
| Return value | Effect | Downstream result |
|---|---|---|
void or undefined |
Continue without changing context | Next middleware sees the same context type. |
Response |
Stop request-phase processing | Response callbacks already registered still run. |
RequestPlugin object |
Merge plugin data into context | Later middleware can read added properties and env values. |
Return nothing for observation-only middleware:
import { chain } from 'alien-middleware'
const app = chain().use(context => {
console.log(context.request.method, context.url.pathname)
})
Return a Response to end the request:
const app = chain().use(context => {
if (!context.request.headers.has('Authorization')) {
return new Response('Unauthorized', { status: 401 })
}
})
Add Context Properties
Return a request plugin to add properties for later middleware. Define the middleware inline when you want TypeScript to infer the downstream context.
const app = chain()
.use(() => {
return { user: { id: 'u_123', name: 'Ada' } }
})
.use(context => {
return new Response(`Hello, ${context.user.name}`)
})
Plugin properties are defined on the active request context. If a later plugin uses the same property name, the later property descriptor replaces the earlier one.
Add Environment Values
Use the reserved env plugin key when middleware computes or loads values that
should be read through context.env().
const app = chain()
.use(() => {
return { env: { FEATURE_FLAG: 'enabled' } }
})
.use(context => {
return new Response(context.env('FEATURE_FLAG'))
})
Environment plugins are layered over host environment lookup. If a plugin
provides a key, context.env(key) returns the plugin value; otherwise it
delegates to the previous environment accessor.
Set Response Headers
Use context.setHeader() in request middleware when a header should be applied
to the final response, regardless of which later middleware creates it.
const app = chain()
.use(context => {
context.setHeader('X-Powered-By', 'alien-middleware')
})
.use(() => new Response('ok'))
If multiple middleware set the same header, the later value wins.
Observe or Replace Responses
Register response callbacks with context.onResponse() or with the reserved
onResponse plugin key. A callback can mutate response headers, return nothing,
or return a replacement Response.
const app = chain()
.use(context => {
context.onResponse(response => {
response.headers.set('X-Trace', 'present')
})
})
.use(() => new Response('ok'))
For background work that should not delay the response, schedule the promise
through context.waitUntil().
declare function logResponse(response: Response): Promise<void>
const app = chain().use(context => {
context.onResponse(response => {
context.waitUntil(logResponse(response))
})
})
Important
Response callbacks only exist after their middleware runs. If an earlier
middleware returns a Response, callbacks registered by later middleware are
not installed.
Merge Chains
Pass one chain to .use() when a reusable group of middleware should run in the
same context as the parent chain.
const session = chain().use(() => {
return { session: { id: 's_123' } }
})
const app = chain()
.use(session)
.use(context => {
return new Response(context.session.id)
})
The nested chain's plugin output is visible to middleware after .use(session).
Isolate Chains
Call .isolate() when a reusable chain should be allowed to return a response
but should not leak plugin properties to the parent chain.
const privateChain = chain().use(() => {
return { internalOnly: true }
})
const app = chain()
.use(privateChain.isolate())
.use(context => {
return new Response('internalOnly' in context ? 'leaked' : 'isolated')
})
If an isolated chain does not return a response, the parent chain continues with the next middleware.
Skip Remaining Middleware
Use context.passThrough() to stop the current chain without creating a
response.
const app = chain()
.use(context => {
if (context.url.pathname !== '/health') {
return context.passThrough()
}
return new Response('ok')
})
.use(() => {
throw new Error(
'This middleware is skipped when the first middleware exits'
)
})
In a final fetch handler, an unresolved request becomes 404 Not Found. In an
isolated chain, passThrough() skips the rest of the isolated chain and lets
the parent continue.
If the Xenomorph returns a Response, downstream middleware does not get a dramatic hallway scene.