Documentation
Body Parsing
Kynetra FX exposes three body-parsing helpers on ctx: jsonBody(), textBody(), and formBody(). They are lazy — the body is not parsed until you call one of them. Each returns a Promise that resolves to the parsed value.
JSON body#
ctx.jsonBody<T>() parses the request body as JSON and returns it typed as T. When no type parameter is supplied, the return type is unknown.
import { createFX } from '@kynetra/fx' const app = createFX() app.post('/users', async (ctx) => { const body = await ctx.jsonBody<{ name: string; email: string }>() // body.name and body.email are typed as string return ctx.json({ created: true, name: body.name }, { status: 201 })})Tip
app.route() with an input schema) instead. It calls jsonBody() internally, validates the result, and exposes it as the fully-typed ctx.input. See Contracts.Text body#
ctx.textBody() reads the body as a raw UTF-8 string. Useful for webhooks that send plain text, XML, or non-JSON payloads.
app.post('/webhook', async (ctx) => { const raw = await ctx.textBody() // string // Verify HMAC signature, then parse as needed const sig = ctx.header('x-signature') const expected = await hmac(raw, ctx.env.WEBHOOK_SECRET) if (sig !== expected) { return ctx.json({ error: 'bad signature' }, { status: 401 }) } const data = JSON.parse(raw) return ctx.json({ received: true })})Form data#
ctx.formBody() parses both application/x-www-form-urlencoded and multipart/form-data bodies, returning a standard FormData object.
// URL-encoded form (e.g. <form method="post">)app.post('/contact', async (ctx) => { const form = await ctx.formBody() const name = form.get('name') as string const message = form.get('message') as string return ctx.json({ name, message })})File uploads (multipart)#
Multipart form data with file uploads also goes through ctx.formBody(). File fields are returned as standard File instances (a subclass of Blob).
app.post('/upload', async (ctx) => { const form = await ctx.formBody() const file = form.get('file') as File | null if (!file) { return ctx.json({ error: 'no file' }, { status: 400 }) } // file.name, file.size, file.type are available const bytes = await file.arrayBuffer() const buffer = new Uint8Array(bytes) // Store in R2 / Blob adapter... return ctx.json({ name: file.name, size: file.size, type: file.type, }, { status: 201 })})Note
Body caching#
The underlying Request body can only be consumed once. Kynetra FX does not cache the parsed body between calls — calling jsonBody() twice will throw on the second call because the stream has already been consumed.
app.post('/double', async (ctx) => { const body = await ctx.jsonBody() // Do NOT call jsonBody() again — the stream is consumed. // Store the result in a variable and reuse it: const validated = validate(body) return ctx.json(validated)})If multiple middleware layers need to inspect the body, parse it once and attach the result to ctx.state or use a context decoration:
// Parse body once in middleware, attach to stateexport const parseBody: FXMiddleware = async (ctx, next) => { const contentType = ctx.header('content-type') ?? '' if (contentType.includes('application/json')) { ctx.state.set('body', await ctx.jsonBody()) } return next()} // Handler reads from state — no second parseapp.post('/items', parseBody, (ctx) => { const body = ctx.state.get('body') as any return ctx.json({ name: body.name })})Raw body access#
For cases where none of the helpers fit, access the body directly via the standard Request API:
app.post('/binary', async (ctx) => { const buffer = await ctx.req.arrayBuffer() // ArrayBuffer const bytes = new Uint8Array(buffer) return ctx.json({ byteLength: bytes.byteLength })})Content-Type checking#
Kynetra FX does not automatically reject requests with mismatched Content-Type headers — that is your responsibility. Check before parsing when your handler is strict about format:
app.post('/strict', async (ctx) => { const ct = ctx.header('content-type') ?? '' if (!ct.includes('application/json')) { return ctx.json( { error: { code: 'FX_VALIDATION_ERROR', message: 'Expected application/json' } }, { status: 415 } ) } const body = await ctx.jsonBody() return ctx.json({ ok: true, body })})