IngeniumContext

IngeniumContext is the single argument your handlers receive. It's a typed, pooled object with lazy getters for things like query and body, chainable setters for response headers, and explicit writers for when you want full control.

API surface

class IngeniumContext<Params = Record<string, string>> {
  // Request
  method: HttpMethod              // 'GET' | 'POST' | ...
  url: string                     // path + ?query
  path: string                    // no query
  rawQuery: string
  query: URLSearchParams          // lazy
  params: Params                  // route params
  headers: IncomingHttpHeaders    // lowercased per Node convention
  body: IngeniumBody                   // lazy parsers
  state: Record<string, unknown>  // per-request scratch

  // Network info (trust-proxy aware)
  ip: string                      // client IP (XFF-aware if trustProxy enabled)
  ips: readonly string[]          // full forwarded chain
  protocol: 'http' | 'https'
  secure: boolean
  hostname: string
  remoteAddress: string           // immediate socket peer
  baseProtocol: 'http' | 'https'  // underlying transport

  // Response setters (chainable)
  status(code: number): this
  set(name: string, value: string | string[]): this
  setHeader(name: string, value: string | string[]): this
  getHeader(name: string): string | string[] | undefined

  // Response writers
  json(body: unknown, status?: number): void
  text(body: string, status?: number): void
  html(body: string, status?: number): void
  send(body: Buffer | string, status?: number): void
  redirect(location: string, status?: number): void  // default 302
  stream(readable: Readable, contentType?: string): void
}

The class is pool-bound: one instance per pool slot, reused across requests. reset() zeros every field by reassignment to keep the V8 hidden class stable.

Response reflection

Handlers can return a value instead of calling a writer:

Return valueWire output
undefined / null204 No Content
string starting <200 text/html
other string200 text/plain
Buffer / Uint8Array200 application/octet-stream
Readable200 streamed
any object/array200 application/json

If a ctx.json/text/html/send/redirect/stream helper has been called, the return value is ignored.

Per-request state

ctx.state is the scratch space for per-request data - request IDs, the resolved user, timing marks, anything you want to thread through middleware and handlers. It's a plain Record<string, unknown> that's cleared between requests when the context is recycled.

For typed access patterns (ctx.user, ctx.session), use a plugin decorator and a module augmentation rather than poking values onto ctx.state.

Header injection guard

ctx.set(name, value) rejects any value containing \r\n immediately and throws IngeniumHeaderInjectionError. This catches CRLF injection at the call site rather than deep inside Node's wire path.

Where to next?

  • Body parsing - ctx.body.json(), multipart, schema validation.
  • Trust-proxy - making ctx.ip and friends honor X-Forwarded-*.
  • Plugins - decorating ctx with typed fields.
  • Errors - what gets thrown and how to catch it.