Reusable middleware chains with top-notch TypeScript support. Built on Web Standard APIs so chains can run behind any fetch-compatible server.
alien-middleware is built on a few key principles:
-
Best-in-class TypeScript support - Type safety is a first-class citizen. The library provides strong type inference for middleware chains, context extensions, and environment variables.
-
Web Standards - Built on standard Web APIs like
RequestandResponse, allowing you to write idiomatic code that follows established patterns and conventions. -
No vendor lock-in - Middleware uses Web
RequestandResponseobjects, with a fetch adapter for servers such as srvx across Node.js, Deno, Bun, Cloudflare Workers, and more. -
Linear middleware flow - Unlike Express-style middleware, there's no
next()function to call. Middleware either returns aResponse(ending the chain) or doesn't (continuing to the next middleware). This makes the flow easier to reason about and eliminates common bugs like forgetting to callnext(). -
Immutable chains - Middleware chains are immutable, making them easier to compose, extend, and reason about.
First, add the package to your project:
pnpm add alien-middlewareImport the chain function and initialize a middleware chain. You can optionally provide an initial middleware directly to chain.
import { chain } from 'alien-middleware'
// Create an empty chain
const app = chain()
// Or create a chain with an initial middleware
const appWithInitial = chain(context => {
console.log('Initial middleware running for:', context.request.url)
})Use the .use() method to add middleware functions to the chain.
import type { RequestContext } from 'alien-middleware'
const firstMiddleware = (context: RequestContext) => {
console.log('First middleware')
// Doesn't return anything, so the chain continues
}
const secondMiddleware = (context: RequestContext) => {
console.log('Second middleware')
return new Response('Hello from middleware!', { status: 200 })
// Returns a Response, terminating the request-phase chain
}
// Add middleware sequentially
const app = chain().use(firstMiddleware).use(secondMiddleware)Note
Middleware chains are immutable. Each call to .use() returns a new chain instance.
To run the middleware chain with srvx, wrap it with toFetchHandler and pass the result to serve.
import { serve } from 'srvx'
import { toFetchHandler } from 'alien-middleware/srvx'
const app = chain()
.use(mySessionMiddleware)
.use(myAuthMiddleware)
.use(myLoggerMiddleware)
serve({
port: 3000,
fetch: toFetchHandler(app),
})Note
If no middleware in the chain returns a Response, a 404 Not Found response
is automatically returned.
If you add the same middleware multiple times, it will only run once. This is a safety measure that allows you to use the same middleware in different places without worrying about it running multiple times.
const app = chain().use(myMiddleware).use(myMiddleware)Request middleware runs sequentially before a Response is generated.
-
Terminating the Chain: Return a
Responseobject to stop processing subsequent request middleware.import type { RequestContext } from 'alien-middleware' const earlyResponder = (context: RequestContext) => { if (context.request.url.endsWith('/forbidden')) { return new Response('Forbidden', { status: 403 }) } // Otherwise, continue the chain }
-
Extending Context: Return an object (known as a "request plugin") to add its properties to the context for downstream middleware. Getter syntax is supported.
import type { RequestContext } from 'alien-middleware' type User = { id: number; name: string } const addUser = (context: RequestContext) => { // In a real app, you might look up a user based on a token const user: User = { id: 1, name: 'Alice' } return { user } } const greetUser = (context: RequestContext<{}, { user: User }>) => { // The `user` property is now available thanks to `addUser` return new Response(`Hello, ${context.user.name}!`) } const app = chain().use(addUser).use(greetUser)
-
Extending Environment: Request plugins may have an
envproperty to add environment variables accessible viacontext.env().import type { RequestContext } from 'alien-middleware' const addApiKey = (context: RequestContext) => { return { env: { API_KEY: 'secret123' } } } const useApiKey = (context: RequestContext<{ API_KEY: string }>) => { const key = context.env('API_KEY') console.log('API Key:', key) // Output: API Key: secret123 } const app = chain().use(addApiKey).use(useApiKey)
-
Setting Response Headers: Call the
context.setHeader()method to set a response header.const app = chain().use(context => { context.setHeader('X-Powered-By', 'alien-middleware') })
Note
If you're wondering why you need to return an object to define properties (rather than simply assigning to the context object), it's because TypeScript is unable to infer the type of the context object downstream if you don't do it like this.
Another thing to note is you don't typically define middlewares outside the
.use(…) call expression, since that requires you to unnecessarily declare
the type of the context object. It's better to define them inline.
Request middleware can register a response callback to receive the Response object. This is done by either returning an onResponse method or by calling context.onResponse(callback). Response callbacks may return a new Response object.
Response callbacks are called even if none of your middlewares generate a Response. In this case, they receive the default 404 Not Found response. Note that isolated middleware chains are an exception to this rule, since a default response is not generated for them.
// Approach 1: Returning an `onResponse` method
const poweredByMiddleware = (context: RequestContext) => ({
onResponse(response) {
response.headers.set('X-Powered-By', 'alien-middleware')
},
})
const mainHandler = (context: RequestContext) => {
// Approach 2: Calling `context.onResponse(callback)`
context.onResponse(response => {
assert(response instanceof Response)
})
return new Response('Main content')
}
const app = chain().use(poweredByMiddleware).use(mainHandler)
const response = await app({…})
console.log(response.headers.get('X-Powered-By')) // Output: alien-middlewareNote
Remember that request middlewares may not be called if a previous middleware
returns a Response. In that case, any response callbacks added by the
uncalled middleware will not be executed. Therefore, order your middlewares
carefully.
Even if a middleware returns an immutable Response (e.g. from a fetch() call), your response callback can still modify the headers. We make sure to clone the response before processing it with any response callbacks.
To ensure the client receives a response as soon as possible, your response callbacks should avoid using await unless absolutely necessary. Prefer using context.waitUntil() to register independent promises that shouldn't block the response from being sent.
const app = chain().use(context => {
context.onResponse(async response => {
// ❌ Bad! This blocks the response from being sent.
await myLoggingService.logResponse(response)
// ❌ Bad! This may be interrupted by serverless runtimes.
myLoggingService.logResponse(response).catch(console.error)
// ✅ Good! This doesn't block the response from being sent.
context.waitUntil(myLoggingService.logResponse(response))
})
})By passing a middleware chain to .use(), you can merge it with the existing chain. Its middlewares will be executed after any existing middlewares in this chain and before any new middlewares you add later.
const innerChain = chain((context: RequestContext) => {
return { helloFromInner: true }
})
const app = chain()
.use(innerChain)
.use(context => {
context.helloFromInner // Output: true
})When adding a middleware chain to another, you may use the isolate() method to isolate the nested chain from the outer chain.
const isolatedChain = chain().isolate()This prevents the nested chain from affecting middleware in the outer chain (e.g. through request plugins).
const innerChain = chain()
.use(() => ({
foo: true,
}))
.use(context => {
context.foo // Output: true
})
const outerChain = chain()
.use(innerChain.isolate())
.use(context => {
context.foo // Output: undefined
})If an isolated chain does not return a Response, execution continues with the next middleware in the outer chain.
To stop processing a request (e.g. skip any remaining middlewares), use the context.passThrough() method. In a final fetch handler, unresolved requests become the default 404 Not Found response.
In the context of an isolated chain, context.passThrough() will skip remaining middlewares in the isolated chain, but the outer chain will continue execution with the next middleware.
const app = chain()
.use(context => {
if (!context.request.headers.has('Authorization')) {
// It's best practice to return immediately after
// calling passThrough()
return context.passThrough()
}
return new Response('Authorized', { status: 200 })
})
.use(context => {
// This will never run, since the previous middleware
// either returns a Response or calls passThrough()
throw new Error('This request middleware will never run')
})When reading environment values without this package, the return type is typically string | undefined, which means you either need to write defensive checks or use type assertions. Neither is ideal.
With alien-middleware, you must declare an environment variable's type in order to use it.
import { chain } from 'alien-middleware'
// A common pattern is to declare a dedicated type for the environment variables.
type Env = {
API_KEY: string
}
const app = chain<Env>().use(context => {
const key = context.env('API_KEY')
// ^? string
})When defining a middleware, you can declare env types that the middleware expects to use.
import type { RequestContext } from 'alien-middleware'
// Assuming `Env` is defined like in the previous example.
const myMiddleware = (context: RequestContext<Env>) => {
const key = context.env('API_KEY')
}In both examples, we only declare the environment type because we're not using any additional context properties. The second type parameter is for context properties. The third is for runtime-specific host data available at context.host.runtime. On principle, middleware should avoid using runtime-specific data unless you write fallback logic for other hosts.
The request context exposes host-provided data through context.host.
const app = chain().use(context => {
const ip = context.host.ip
const runtimeName = context.host.runtime?.name
context.waitUntil(myLogger.log({ ip, runtimeName }))
})When using alien-middleware/srvx, request.ip, request.runtime, and request.waitUntil are copied from the srvx ServerRequest into context.host.
The routes function provides a way to create a router instance for handling different paths and HTTP methods.
import { routes } from 'alien-middleware/router'
const router = routes()The routes function leverages pathic to provide type inference for path parameters.
import { routes } from 'alien-middleware/router'
const router = routes()
router.use('/users/:userId', context => {
// context.params.userId is automatically typed as string
const userId = context.params.userId
return new Response(`User ID: ${userId}`)
})You can specify one or more HTTP methods for a route by providing the method(s) as the first argument to .use().
import { routes } from 'alien-middleware/router'
const router = routes()
// This handler will only run for GET requests to /api/items
router.use('GET', '/api/items', context => {
return new Response('List of items')
})
// This handler will only run for POST requests to /api/items
router.use('POST', '/api/items', context => {
return new Response('Create a new item', { status: 201 })
})
// This handler will run for both PUT and PATCH requests to /api/items/:id
router.use(['PUT', 'PATCH'], '/api/items/:id', context => {
const itemId = context.params.id
return new Response(`Update item ${itemId}`)
})
// This handler will run for any method to /status
router.use('/status', context => {
return new Response('Status: OK')
})Note
Your routes don't need to be in any particular order, unless their path
patterns are exactly the same. The pathic library will match the most
specific path first. This allows you to split your routes into multiple files
for better organization.
You can pass a middleware chain to the routes() function to apply middleware specifically to the routes defined by that router instance. This provides type safety for context extensions within the router.
import {
chain,
type RequestContext,
type RequestPlugin,
} from 'alien-middleware'
import { routes } from 'alien-middleware/router'
// Define a middleware that adds a user to the context
const addUserMiddleware = (context: RequestContext): RequestPlugin => {
const user = { id: 123, name: 'Alice' }
return { user }
}
// Create a chain with the middleware
const authMiddlewares = chain(addUserMiddleware)
// Pass the chain to the routes function
const authenticatedRouter = routes(authMiddlewares)
// Define a route that uses the context property added by the middleware
authenticatedRouter.use('/profile', context => {
// context.user is now type-safe and available
return new Response(`Welcome, ${context.user.name}!`)
})
// Routes defined on a router without a chain won't have the user property
const publicRouter = routes()
publicRouter.use('/public', context => {
// context.user is not available here
// @ts-expect-error - user is not defined on this context
console.log(context.user)
return new Response('Public content')
})
// You can combine routers using .use()
const app = chain().use(authenticatedRouter).use(publicRouter)