Concepts

Type Inference

Strong inference comes from keeping middleware output close to .use() and using exported utility types only at API boundaries.

Chain Type Metadata

Every middleware chain carries type metadata for three things:

Metadata Meaning Read through
env Environment variables available through context.env() RequestContext<TEnv>
properties Plugin properties added to the context object RequestContext<TEnv, TProperties>
runtime Host runtime metadata at context.host.runtime RequestContext<TEnv, TProperties, TRuntime>

The chain generic parameters set the starting point for a chain.

import { chain } from 'alien-middleware'

type Env = {
  API_KEY: string
}

type Runtime = {
  name: 'node'
}

const app = chain<Env, {}, Runtime>().use(context => {
  context.env('API_KEY')
  context.host.runtime?.name
})

Prefer Inline Middleware

Inline middleware lets TypeScript use each return value to infer downstream context additions.

const app = chain()
  .use(() => ({ user: { id: 'u_123' } }))
  .use(context => {
    return new Response(context.user.id)
  })

If you pull middleware into a standalone function, annotate the input context only when the function must be reused independently.

import type { RequestContext, RequestPlugin } from 'alien-middleware'

type UserPlugin = RequestPlugin & {
  user: { id: string }
}

function addUser(_context: RequestContext): UserPlugin {
  return { user: { id: 'u_123' } }
}

const app = chain().use(addUser)

Extract Context Types

Use MiddlewareContext when another function needs the context produced by a chain or by a list of middleware.

import type { MiddlewareContext } from 'alien-middleware'

const app = chain().use(() => ({ user: { id: 'u_123' } }))

function renderProfile(context: MiddlewareContext<typeof app>) {
  return new Response(context.user.id)
}

Use RouterContext when a function should accept the context type for a router.

import { routes, type RouterContext } from 'alien-middleware/router'

const router = routes(app)

function fromRouter(context: RouterContext<typeof router>) {
  return context.user.id
}

Compose Middleware Types

Use ApplyMiddlewares when a library helper receives a fixed tuple of middleware and needs to expose the resulting chain metadata.

import type {
  ApplyMiddlewares,
  Middleware,
  RequestHandler,
} from 'alien-middleware'

type Pair = [Middleware, Middleware]

type PairHandler = RequestHandler<ApplyMiddlewares<Pair>>

For most application code, a value-level chain is easier to read than a manually constructed type.

Avoid Runtime-Specific Types by Default

Runtime metadata is useful at adapter boundaries. Middleware that can run on several hosts should keep the runtime generic unless it has fallback behavior.

const portable = chain().use(context => {
  const runtimeName = context.host.runtime?.name ?? 'unknown'
  return new Response(runtimeName)
})

Use filterRuntime when a branch should only continue for one runtime.

import { filterRuntime } from 'alien-middleware'

const nodeOnly = chain()
  .use(filterRuntime<{ name: 'node' }>('node'))
  .use(context => {
    return new Response(context.host.runtime?.name)
  })

filterRuntime narrows context.host.runtime through a type-level marker. It does not add context.runtime as a runtime property.

Extending RequestContext

When using RequestContext on the right side of an extends clause, prefer RequestContext<any> over bare RequestContext. The default type parameters are intentionally stricter than any in some positions.

import type { RequestContext } from 'alien-middleware'

interface AppContext extends RequestContext<any> {
  user: { id: string }
}

The type system is doing the Vulcan mind meld, but thankfully with fewer hand poses.