Askr
Server & APIs

Request Binding

Use request binding with the current published Askr packages.

  • @askrjs/askr0.0.59

How to use request binding

Start with this complete request binding shape, then replace the example data and application service with your own.

  1. Import the published @askrjs/askr entrypoint.
  2. Keep request binding configuration next to the component, route, or server composition root that owns it.
  3. Handle pending, unavailable, cancellation, and failure states, then verify the production path.
router.post('/projects', async (context) => {
  const input = await context.bind<CreateProjectInput>();
  return created(await projects.create(input));
});

Bind from context

ctx.bind<T>() reads whatever supported body the request has, merges it with the query string and route params, and caches the result — call it more than once in the same handler and you get the same object back rather than a second read of the body. The standalone bind(context) function takes a plain BindContext (request, params, url, query) instead of a full ServerContext, which is what lets the same binding logic run somewhere that only has those four pieces, like a test. The generic type argument is purely a TypeScript view of the shape you expect back; bind() does not check the request against it at runtime.

Supported inputs

Three body types are understood: application/json (and any +json media type, provided the root is an object), application/x-www-form-urlencoded, and multipart/form-data, where text fields come back as strings and file fields as File objects. Repeated keys become arrays in both the query string and form bodies — /users?tag=admin&tag=editor binds tag as ["admin", "editor"] — while route parameters are always single decoded strings. GET and HEAD requests never have their body read at all, since neither method is expected to carry one.

Binding errors

A BindingError carries a fixed status of 400 and an optional field naming which part of the request caused the failure, so a caller can tell a malformed body apart from one that simply doesn't match what the handler expected. It's thrown for invalid JSON, a JSON body whose root isn't an object, malformed multipart data, and for trying to read a body that's already been consumed. Because ctx.bind() surfaces these through the normal problem+json response path rather than throwing past your handler, a try/catch is only needed if you want to customize the 400 response yourself.

Validation boundary

Binding is deliberately shallow: it won't parse dotted keys into nested objects, coerce a query string "42" into a number, or check values against a schema — the generic type parameter is a hint for your editor, not a runtime guarantee. Headers are excluded from the bound model entirely; read something like an If-Match value directly with ctx.headers.get("if-match") rather than expecting bind() to surface it. Because untrusted input passes through bind() unchanged, run it through a real validator like @askrjs/schema before relying on any of its values, especially anywhere one flows into a database query or a downstream call.