Migration from Express

Ingenium keeps the Express mental model - app.get, app.use, mountable routers, drop-in middleware - but swaps (req, res, next) for a typed ctx and lets handlers return values. The migration cost is intentionally small.

The 5-minute Express to Ingenium diff

// Express                                      // Ingenium
import express from 'express'                   import { ingenium } from 'ingenium'
const app = express()                           const app = ingenium()

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

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

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

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

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

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

app.listen(3000)                                await app.listen(3000)

Breakable changes

  1. Handlers may return values. return obj is res.json(obj); return 'text' is res.text(...). Calling ctx.json(...) explicitly still works.
  2. Body parsing is lazy. app.use(ingenium.json()) is a no-op stub for ergonomics; the actual parse happens in ctx.body.json() inside your handler.
  3. ctx.state is the per-request scratch space, not ctx.user = ... directly (though plugins can decorate ctx to enable that).

That's the whole list. Everything else from the Express mental model carries over verbatim.

Express middleware that still works

If you depend on existing Express middleware (cors, helmet, cookie-parser, etc.), use the ingenium-compat shim - it wraps (req, res, next) middleware so they run inside the Ingenium chain. The full compatibility matrix lives on that page.

Where to next?