v0.0.1 · alpha

Express.js ergonomics,
built for the next decade.

A typed HTTP framework for Node 20+ and Bun 1.1+. The shortest path from a working Express.js app to a modern, typed server - same shape, typed ctx, current-decade router.

$npm install ingenium
node 20+·bun 1.1+·MIT

Show me the code.

A full server. No res.send. No body-parser. Return a value and Ingenium reflects it to the wire.

server.ts TypeScript
import { ingenium } from 'ingenium'

const app = ingenium()

app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  console.log(`${ctx.method} ${ctx.path} → ${Date.now()-start}ms`)
})

// Return a value → reflected to the wire
app.get('/', () => 'hello')
app.get('/users/:id', (ctx) => ({ id: ctx.params.id }))
app.post('/echo', async (ctx) => await ctx.body.json())

const server = await app.listen(3000)
console.log(`listening on :${server.port}`)
~ / curl
$ curl localhost:3000/
hello

$ curl localhost:3000/users/42
{"id":"42"}

$ curl -XPOST localhost:3000/echo \
    -H 'content-type: application/json' \
    -d '{"hello":"world"}'
{"hello":"world"}

# server stdout:
GET / → 1ms
GET /users/42 → 0ms
POST /echo → 2ms
Why Ingenium

Fix Express's three structural problems - without a new mental model.

Linear routing, untyped req/res, and per-request allocation. Same shape, different engine.

Pain pointExpressHono / Fastify
Ingenium
Router speed at 1000 routesO(n) linear scanO(k) trieO(k) radix trie + wildcard backtrack
req / res typesany in practicestrict, but unfamiliar surfacestrict, Express-shaped
Per-request allocationnew req/res/next each requestvariespooled IngeniumContext, lazy getters
Middleware compositionre-walked per requestcompose-on-registerlazy compose with dirty-bit recompose
Body parsingbody-parser always runsalways-on parsinglazy via ctx.body.json()
Default body size limit100 KBvaries100 KB (matches Express)
Bun supportcommunity shimvariesfirst-class adapter
Migration cost from Expressn/ahighlow

The pitch in one sentence: the shortest path from a working Express app to throughput competitive with Hono and Fastify.

5-minute diff

Express Ingenium

Most of your code stays put. Handlers can return values, body parsing is lazy, per-request state lives on ctx.state. That's the list.

express.ts
before
import express from 'express'
const app = express()

app.use(express.json())

app.use((req, res, next) => {
  req.startedAt = Date.now()
  next()
})

app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id })
})

app.post('/users', (req, res) => {
  const body = req.body
  res.status(201).json(user)
})

const router = express.Router()
router.get('/health',
  (req, res) => res.json({ ok: 1 }))
app.use('/api', router)

app.use((err, req, res, next) => {
  res.status(500).json({ err: err.message })
})

app.listen(3000)
ingenium.ts
after
import { ingenium } from 'ingenium'
const app = ingenium()

app.use(ingenium.json())  // (no-op, parsing is lazy)

app.use(async (ctx, next) => {
  ctx.state.startedAt = Date.now()
  await next()
})

app.get('/users/:id', (ctx) =>
  ({ id: ctx.params.id }))


app.post('/users', async (ctx) => {
  const body = await ctx.body.json()
  return ctx.json(user, 201)
})

const router = ingenium.Router()
router.get('/health',
  () => ({ ok: 1 }))
app.use('/api', router)

app.onError((err, ctx) => {
  ctx.json({ err: err.message }, 500)
})

await app.listen(3000)
Production hardening

Primitives an API team actually needs.

All opt-in. Native, not glued on. Each one is here because it fixes a real production incident pattern.

Timeout ceiling

Per-request timeout with IngeniumTimeoutError (503). A handler that never resolves no longer leaks the context, socket, and pool slot.

Hard body cap

maxRequestBytes enforced at the transport layer - before any consumer touches a byte. Works even when handlers stream the body.

Header injection guard

ctx.set(name, value) rejects CRLF immediately at the call site, not deep inside Node's wire path. Catches injection at the source.

JWT + JWKS

Asymmetric JWT (RS/PS/ES + JWKS) for Auth0, Okta, Cognito, Clerk, Supabase. Algorithm-confusion blocked, 'none' rejected unconditionally.

Late-write protection

_epoch counter on IngeniumContext detects orphaned-handler writes after a timeout. Stops cross-request response corruption on pool recycle.

Idempotency

ingenium.idempotency() with pluggable store. Default skips caching 5xx - transient errors don't get replayed for the entire TTL.

CSRF protection

Double-submit cookie (default) or synchronizer pattern with HMAC-signed tokens. Timing-safe verification, secret rotation supported.

Sessions

HMAC-SHA256-signed cookies, 144-bit ids, regenerate() for fixation defense, pluggable SessionStore - Redis-ready interface.

Transports

One app, any wire.

Pluggable transport adapters. Write once, swap the engine.

Node httpdefault
Bun.servefirst-class
HTTP/2h2 + h2c
WebSocketws peer
Server-Sent Eventsnative

Ship your next API on Ingenium.

Same Express mental model. Current-decade router. Typed ctx. Production-grade primitives opt-in. Ten minutes from npm install to shipping.

$npm install ingenium

MIT licensed · alpha · made for Node 20+ and Bun 1.1+