Kynetra FX

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.

app.ts
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

For validated, auto-typed bodies, use the contract layer (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.ts
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.

app.ts
// 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.ts
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

On Cloudflare Workers, request bodies are limited to 100 MB by default. Large file uploads should be handled via direct R2 presigned URLs rather than proxied through a Worker.

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.ts
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:

middleware/body.ts
// Parse body once in middleware, attach to state
export 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 parse
app.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.ts
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.ts
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 })
})