: the method is
half of a route's identity, and an application that remembers its
routes — for a generated client, for tooling — needs to know which one
this is.
method: "PATCH",
path: "/notes/:id"
Route path with :param segments, e.g. "/orders/:id/cancel".
Must start with /, contain no empty segments and no trailing slash;
a malformed literal is a compile error.
path: "/notes/:id",
schema: { params: NoteId, body: NoteChange },
handler: (
ctx: {
readonly out: Outgoing;
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {
id: number;
};
readonly body: {
title?: string |undefined;
body?: string |undefined;
};
}
ctx) => ({
id: number
id:
ctx: {
readonly out: Outgoing;
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {
id: number;
};
readonly body: {
title?: string |undefined;
body?: string |undefined;
};
}
ctx.params.
id: number
id, ...
ctx: {
readonly out: Outgoing;
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {
id: number;
};
readonly body: {
title?: string |undefined;
body?: string |undefined;
};
}
ctx.
body: {
title?: string |undefined;
body?: string |undefined;
}
body }),
params: {
id: number;
}
}),
}));
A value the schema converts arrives converted: ctx.params.id is a
number here.
A part without a schema is neither read nor typed. Without a query
schema the query string is not parsed and ctx.query does not exist.
Without a body schema, bodyType or rawBody, the body is never read.
Context explains why.
If a beforeValidation hook changes ctx.body or ctx.query, the schema
validates the hook’s value.
All parts are checked, in the order params, query, headers,
cookies, body, and their issues are collected. The client sees
everything wrong with a request in one answer. A failure answers 422
before the handler runs:
{
"status": 422,
"message": "Validation failed",
"error": "VALIDATION_FAILED",
"issues": [
{ "message": "Invalid option: expected one of \"yes\"|\"no\"", "path": ["query", "draft"] },
{ "message": "Too small: expected string to have >=1 characters", "path": ["body", "title"] }
]
}
Each issue’s path starts with the part, then the keys inside it. The
messages come from the validator and change with its version, so branch
on error and path, and show message to people.
To answer 400 instead, set validation: { status: 400 } on createApp.
A body that is not valid JSON is always a 400. To change the format of
the answer, use an application onError hook; see
Errors.
Path parameters, query values, headers, cookies and form fields all arrive
as strings. The framework does not guess types; the schema converts them.
With Zod, use z.coerce:
Arrays need the same care. A key sent once is a string, and only a
repeated key becomes an array: ?tag=a is "a", and ?tag=a&tag=b is
["a", "b"]. A plain z.array() refuses a single tag, so wrap a single
value in an array first:
const
constone: (value:unknown) =>unknown
one= (
value: unknown
value:unknown) => (typeof
value: unknown
value==="string"? [
value: string
value] :
value: unknown
value);
constFilter= z.object({
tag: z.preprocess(
constone: (value:unknown) =>unknown
one, z.array(z.string())).optional(),
});
Form fields work the same way. With TypeBox, tb(…, { convert: true })
does both conversions: "42" to 42, and "a" to ["a"].
Specifies the minimum number of characters allowed in the string.
Must be a non-negative integer.
minLength: 1 }),
pinned: Type.TBoolean
pinned: Type.Boolean() }));
route({
method: "POST"
The method this route answers.
Kept as a literal rather than widened to
Method
: the method is
half of a route's identity, and an application that remembers its
routes — for a generated client, for tooling — needs to know which one
this is.
method: "POST",
path: "/notes"
Route path with :param segments, e.g. "/orders/:id/cancel".
Must start with /, contain no empty segments and no trailing slash;
a malformed literal is a compile error.
path: "/notes",
schema?: {
readonly body: TypeBoxSchema<Type.TObject<{
title:Type.TString;
pinned:Type.TBoolean;
}>>;
} |undefined
Validation schemas; an absent part is neither parsed nor typed.
schema: {
body: TypeBoxSchema<Type.TObject<{
title:Type.TString;
pinned:Type.TBoolean;
}>>
body:
constWithTypeBox:TypeBoxSchema<Type.TObject<{
title:Type.TString;
pinned:Type.TBoolean;
}>>
WithTypeBox },
handler: (
ctx: {
readonly params: {};
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly body: {
title: string;
pinned: boolean;
};
}
ctx) =>
ctx: {
readonly params: {};
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly body: {
title: string;
pinned: boolean;
};
}
ctx.body,
body: {
title: string;
pinned: boolean;
}
});
tb() options such as convert, clean and defaults are on the
package page. For a schema to appear in the
OpenAPI document it must also produce JSON Schema. Zod, ArkType and
tb() do this themselves; Valibot needs toStandardJsonSchema from
@valibot/to-json-schema. See @tetsujs/openapi.
schema.response checks what the handler returns, at compile time and at
runtime, and the schema’s output is what gets sent: a schema that strips
unknown keys keeps a field like passwordHash out of the response. A
response that fails its schema is a 500, reported to reportError.
Responses covers a schema per
status, what is serialized, and turning the runtime check off with
validateResponses: false.