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.