Express compatibility shim

The ingenium-compat package wraps (req, res, next) middleware so it can run inside a Ingenium middleware chain. The shims are real Node streams - req extends stream.Readable, res extends stream.Writable (and is therefore a real EventEmitter) - wired directly to the IngeniumContext. That makes Express middleware a genuine drop-in: body-reading middleware (body-parser, multer) and response-transforming middleware (compression, express-session) all work end-to-end.

Install

npm install ingenium ingenium-compat cors helmet

Usage

import { ingenium } from 'ingenium'
import { expressCompat } from 'ingenium-compat'
import cors from 'cors'
import helmet from 'helmet'

const app = ingenium()
app.use(expressCompat(cors({ origin: 'https://app.example.com' })))
app.use(expressCompat(helmet()))

Compatibility status

Validated end-to-end in packages/ingenium-compat/test/e2e.test.ts:

MiddlewareStatusNotes
corssupportedSimple requests + OPTIONS preflight.
helmetsupportedAll default security headers.
cookie-parsersupportedreq.cookies populated, mirrored to ctx.state.cookies.
morgansupportedEnd-of-request tokens (:status, :response-time) work - res emits finish.
express-rate-limitsupportedreq.ip is populated, so no custom keyGenerator is needed.
compressionsupportedThe downstream response is replayed through the patched res, so the body is gzipped and Content-Encoding is set.
body-parsersupportedReads the real request stream; req.body mirrors to ctx.state.body.
passport.initializesupportedreq._passport propagates to ctx.state.
passport.authenticatepartialres.redirect and cookie writes work; strategies needing a persisted session also need a session store.
express-sessionsupportedWrites Set-Cookie via on-headers and saves on res.end.
multersupportedreq.pipe(busboy) works against the real Readable; req.file mirrors to ctx.state.

How it works

expressCompat(mw) returns a Ingenium middleware that, per request:

  1. Builds a Readable req and a Writable res over the IngeniumContext.
    • Headers and status proxy live to the context, so header-only middleware (cors, helmet) land their changes with no body round-trip.
    • The request body is lazy - the underlying stream is only claimed when the middleware actually reads it, so header-only middleware pay nothing for it.
  2. Runs the middleware:
    • If it wrote the response (res.json/send/end), the chain stops.
    • If it called next(), the downstream chain runs. If the middleware patched res.write/res.end (the compression / express-session pattern), the downstream response is replayed through res so the patch takes effect; otherwise the fast path leaves the response untouched and emits a synthetic finish for observers like morgan.
    • If it called next(err) or threw, the wrapper rejects to the global onError boundary.
  3. Mirrors req.* mutations (req.user, req.body, req.cookies, ...) back into ctx.state before the downstream chain reads them.

Performance

The compat cost is opt-in and localized - paid only on requests that pass through a wrapped middleware. The core ingenium fast paths (O(k) trie routing, compile-time middleware composition, pooled context) are untouched, so native handlers run at full speed. Header-only middleware stay close to free thanks to the live header proxy and lazy body; body-transforming middleware land at roughly Express cost - only where you choose to use them.

Prefer the native primitives when starting fresh

The shim is for reusing existing Express middleware. For new code the Ingenium-native equivalents are integrated more tightly and skip the shim entirely:

Where to next?