HTTP contracts
Typed route, query, body, and response descriptors per handler - validated at the edge before your code runs.
A contract is a per-handler descriptor of route segments, query parameters, request body, and optional response shapes. For each matching request, Flare parses what you declared, then hands typed values to your handler.
Headers are not part of the contract. Read inbound headers from ctx.req.headers. Only route, query, body, response, maxBodyBytes, and signedCookies are descriptor fields.
The alternative this replaces is validation the handler carries itself: a parse call repeated at the top of every handler, or a validation middleware whose result type never reaches the handler, so the handler re-asserts through a cast what was checked one layer up. The check and the annotation are then maintained separately, and when one changes the cast still compiles; the mismatch surfaces as a wrong value inside a handler instead of a 400 at the boundary. A descriptor is one declaration doing both jobs: the parse run at the edge and the static type of scope.input derive from the same fields, so a handler reading route.id as a number is itself the proof the check ran.
Two ways to attach
Section titled “Two ways to attach”| Style | How | Read values |
|---|---|---|
| Inline route | Descriptor fields on HttpRouteOptions (route, query, body, …) | scope.input |
| Controller | static contract = httpContract({ methodName: descriptor, … }) | this.ctx.extract(Contract.methodName) |
You cannot mix inline descriptor fields and a branded contract token on the same route.
httpContract
Section titled “httpContract”import { httpContract } from "@flare-ts/core";
const ApiContract = httpContract({ getUser: { route: { id: int }, response: { 200: UserSchema } }, createUser: { body: UserSchema, response: { 201: UserSchema } },});Keys must match controller handler method names. Extra keys with no handler emit an ORPHANED_CONTRACT_ENTRY warning at build().
Validation timing
Section titled “Validation timing”match route → parse route params (400 on failure) → parse query params (400 on failure) → attach stream body when body uses streaming → before middleware → buffer + JSON validate body (400/413 on failure) → handler → response serializer (when declared)Route and query validation run before before middleware. JSON body validation runs after before, immediately before the handler.
Contract validation failures return 400 or 413. They never enter host.http.error dispatch. See Error handling.
Descriptor shape
Section titled “Descriptor shape”| Key | Validates |
|---|---|
route | Named :id / :slug segments (int, str only at the HTTP edge) |
query | Query-string primitives (str, int, bool, date, arrays, optional) |
body | model(), schema(...), or stream marker |
response | Per-status serializers; strips undeclared fields |
maxBodyBytes | Per-route body cap; overrides global default |
signedCookies | Opt into build-time cookies.secret check |
Schema primitives come from @flare-ts/lib/schema. The stream marker imports from @flare-ts/core.
Inline example
Section titled “Inline example”import { FlareHost, FlareResponse } from "@flare-ts/core";import { node } from "@flare-ts/core/node";import { int, optional, str } from "@flare-ts/lib/schema";
const host = new FlareHost(node);
host.http.get( "/users/:id", { route: { id: int }, query: { include: optional(str) } }, (_ctx, scope) => { const { route, query } = scope.input; return new FlareResponse(200, { id: route.id, include: query.include ?? null, }); },);
const app = host.build();app.run();Controller example
Section titled “Controller example”import { ControllerBase, httpContract } from "@flare-ts/core";import { Get, Post } from "@flare-ts/core/decorators";import { int, model, str } from "@flare-ts/lib/schema";
class CreateUser extends model({ name: str.min(1), email: str }) {}
const ApiContract = httpContract({ getUser: { route: { id: int } }, createUser: { body: CreateUser },});
class ApiController extends ControllerBase { public static override deps = []; public static override state = []; public static override contract = ApiContract;
@Get("/users/:id") getUser() { const { route } = this.ctx.extract(ApiContract.getUser); return this.ok({ id: route.id }); }
@Post("/users") createUser() { const { body } = this.ctx.extract(ApiContract.createUser); return this.ok({ id: 1, name: body.name }); }}Related
Section titled “Related”- Validate a request body
- Request limits and bodies
- Requests:
FlareRequestand body readers