Argument decorators
A handler’s signature declares exactly what it wants — in any order, nothing more. Each parameter decorator injects one thing: a graph-produced value, part of the request envelope, or a stream handle. Some decorators add a dependency edge to the graph (boot-validated); most just read from the request.
The decorators at a glance
Section titled “The decorators at a glance”| Decorator | Injects | Adds a graph dependency? | Forms |
|---|---|---|---|
@needs('user') |
a value produced by a provider/step (boot-validated) | yes | ('key') |
@ctx() |
the whole accumulated context | no | () |
@param(...) |
route params | no | () · ('id') · ('id', schema) |
@query(...) |
parsed query string | no | () · ('q') · (['a','b']) · (schema) |
@body(...) |
parsed body (JSON / urlencoded / multipart) | no | () · ('field') · (schema) |
@headers(...) |
request headers (whole bag or picked) | no | () · ('authorization') · (['a','b']) · (schema) |
@header('name') |
one request header (singular alias of @headers) |
no | ('x-trace') · ('x-count', schema) |
@inbound() |
the incoming WS message channel | no (WS only) | () |
@abort() |
an AbortSignal that fires on disconnect |
no (stream/WS) | () |
@needs — a graph-produced value
Section titled “@needs — a graph-produced value”@needs('token') injects a value a provider or step produces, and declares a dependency edge
in the graph. Its keys are validated at boot: if nothing provides the token, createApp
throws with a clear error instead of serving undefined.
getUser(@needs('user') user: any, @param('id') id: string) { return { requested: id, you: user };}See Dependency injection for how needs/provides build the
graph.
@ctx — the whole context
Section titled “@ctx — the whole context”@ctx() hands you the entire accumulated context — everything providers and steps have merged
in, plus built-ins. Use it to read values like ctx.protocol and ctx.ip (populated when
trustProxy is on).
Envelope decorators — @param, @query, @body, @headers
Section titled “Envelope decorators — @param, @query, @body, @headers”These read from the parsed request. Each envelope decorator has three access forms:
@query()— the whole object@query('q')— one key@query(['q','date'])— a subset
@Get('/:id')getUser(@param('id') id: string) { /* ... */ }@paramreads route params. It always needs a name to say which param it binds (@param('id')).@queryreads the parsed query string. Query values are always strings on the wire — a schema is often the coercion point (see below).@bodyreads the parsed body. It handlesapplication/json,application/x-www-form-urlencoded, andmultipart/form-data(see the multipart note).@headersreads request headers — the whole bag (@headers()), one key (@headers('authorization')), or a subset (@headers(['a','b'])).
@header — one header, singular alias
Section titled “@header — one header, singular alias”@header('name') is the singular alias of @headers: it picks exactly one request header.
who(@header('x-trace') trace: string) { /* ... */ }It also accepts a schema in slot 2 — @header('x-count', schema).
Schema forms — validation & coercion
Section titled “Schema forms — validation & coercion”@body, @query, @headers, and @param accept an optional Standard Schema (zod,
valibot, arktype, …). The value the handler receives is the schema’s parsed/coerced output,
already typed:
create(@body(CreateUser) user: { email: string }) { return { created: user.email };}A failing schema short-circuits the request with 422. See Validation for the full contract (fail-fast order, error shape, async schemas, and caveats).
Multipart access asymmetry
Section titled “Multipart access asymmetry”A request with Content-Type: multipart/form-data makes @body() resolve to { fields, files }
instead of a plain object.
upload(@body() form: MultipartBody) { const name = form.fields.name; // string | string[] const avatar = form.files.avatar; // UploadedFile | UploadedFile[] return { name, size: Array.isArray(avatar) ? undefined : avatar?.size };}Stream decorators — @inbound and @abort
Section titled “Stream decorators — @inbound and @abort”These are for streaming handlers:
@inbound()(WS only) gives you the incoming WebSocket message channel — the client’s messages to consume. A@Wshandler returns the outbound channel.@abort()(stream / WS) hands you anAbortSignalthat fires on disconnect, so you can tear down work when the client goes away.
@Ws('/echo')echo(@inbound() incoming: AsyncIterable<string>) { const out = channel<string>(); (async () => { for await (const msg of incoming) out.push(`echo: ${msg}`); out.close(); })(); return out;}See Streaming for SSE, WebSocket duplex, and channels.
Where to go next
Section titled “Where to go next”- How injected values are produced: Dependency injection.
- The mental model: The graph.
- Validating and coercing injected values: Validation.
- The full API surface: Decorator reference.