Body parsing

Body parsing is lazy in Ingenium. app.use(ingenium.json()) is a no-op stub for ergonomics; the actual parse happens inside your handler the first time you call ctx.body.json(). This means handlers that don't need a body never pay the cost of buffering or parsing one.

The parser surface

ctx.body.json<T>(schema?, maxBytes?: number): Promise<T>
ctx.body.text(maxBytes?: number): Promise<string>
ctx.body.urlencoded(maxBytes?: number): Promise<Record<string, string>>
ctx.body.buffer(maxBytes?: number): Promise<Buffer>
ctx.body.stream(): Readable
ctx.body.multipart(opts?: MultipartOptions): Promise<MultipartResult>

Default maxBytes is 100,000 (matches Express's body-parser default). Override per-call. Body-too-large throws IngeniumPayloadTooLargeError mid-stream (no post-buffer rejection).

Schema validation

ctx.body.json(schema) accepts three validator shapes, detected in this order:

  1. Standard Schema v1 - any validator that exposes ["~standard"].
  2. Zod-like - anything with safeParse(input).
  3. Plain - parse(input): T.
// 1. Standard Schema v1 (any validator with ["~standard"])
import { type } from 'arktype'
const User = type({ name: 'string', email: 'string' })
app.post('/users', async (ctx) => ctx.body.json(User))

// 2. Zod-like safeParse
import { z } from 'zod'
const User = z.object({ name: z.string(), email: z.string().email() })
app.post('/users', async (ctx) => ctx.body.json(User))

// 3. Plain { parse(input): T }
const User = {
  parse(input: unknown): { name: string } {
    if (typeof input !== 'object') throw new Error('expected object')
    return input as { name: string }
  },
}
app.post('/users', async (ctx) => ctx.body.json(User))

Validation failures throw IngeniumValidationError with a fields: Record<string, string> map. Standard Schema v1 issues with structured paths are dot-joined (['user', 'email']'user.email').

See Schema validation for the full breakdown.

Transport-level body cap

The per-call maxBytes doesn't help if a handler reads via ctx.body.stream() and ignores it. For that, set a hard transport-level cap:

const app = ingenium({ maxRequestBytes: 2 * 1024 * 1024 })   // 2 MiB

Bodies that exceed maxRequestBytes are rejected at the transport layer before any consumer touches a byte.

Where to next?