Request lifecycle
A Beignet request moves through a predictable pipeline before your application code runs and before a response is sent. Read this page when you need to know what the framework guarantees around a request — matching, validation, correlation, and who owns each kind of response.
HTTP request
-> adapter request normalization
-> route matching
-> request correlation (request ID + trace context)
-> onRequest hooks
-> request parsing
-> contract request validation
-> context creation
-> beforeHandle hooks
-> route handler or use case
-> route-owned response validation
-> beforeSend hooks (correlation response headers)
-> adapter response serialization
-> afterSend hooks (instrumentation events)This pipeline keeps business responses strict without forcing every route to declare infrastructure responses such as malformed JSON, auth failures, rate limits, unmatched routes, or unexpected failures.
Route matching
Beignet apps register routes centrally in server/index.ts with
defineRoutes. A catch-all app/api/[[...path]]/route.ts file exposes
server.api, so the adapter can dispatch the incoming request to the
registered contract and handler.
Routes are matched by HTTP method and path. Static segments are more specific
than dynamic segments, so /posts/new wins over /posts/:slug regardless of
registration order. Dynamic parameter names do not affect matching —
registering both /items/:id and /items/:slug for the same method is
rejected at startup as ambiguous, along with the other
registration-time guarantees.
If the path matches one or more registered routes but the method does not,
Beignet returns a framework-owned 405 response with
code: "METHOD_NOT_ALLOWED" and an Allow header listing the registered
methods for that path. HEAD is not implicitly mapped to GET: a HEAD
request on a GET-only path gets a 405 with Allow: GET, so metadata-driven
hooks never run a handler for a method the contract did not declare. CORS
preflight onRequest hooks still short-circuit OPTIONS requests before the
405 is produced.
If no route matches the path at all, Beignet returns a framework-owned 404
error response.
Request validation
The runtime parses path params, query params, headers, and JSON body data from the request. It validates each declared schema before your handler runs.
Invalid request data never reaches the handler. It becomes a framework-owned error response with a standard error envelope.
Request instrumentation
The server owns request correlation. You can rely on these outcomes without writing any hooks:
- Every request gets a request ID and W3C trace context — taken from incoming
x-request-idandtraceparentheaders when present, generated otherwise — available to context factories asrequestIdandtrace. - By default the server writes
x-request-idandtraceparentresponse headers, including on streamed responses. requestanderrorevents are recorded to the resolved provider instrumentation port (see Writing a provider) after responses are sent; without an installed sink, headers are still written and events are a no-op.- Correlation is ambient for the request's duration, so instrumentation sinks can correlate events recorded anywhere in the request.
Configure it with the instrumentation option on createServer(...):
const server = await createNextServer({
// ...
instrumentation: {
requestIdHeader: "x-request-id", // or false
traceContextHeader: "traceparent", // or false
ignorePaths: ["/api/devtools"],
redact: (event) => event,
shouldCapture: ({ response }) => response.status !== 404,
},
});Pass instrumentation: false to disable headers and event recording. Context
factories still receive requestId and trace arguments. Context values win
over server-computed correlation: when a factory sets its own requestId,
headers and recorded events use it.
Context and hooks
The server's context.request factory builds the per-request context used by
handlers, hooks, and use cases from the framework-neutral request, final
ports, the matched contract, and the server-resolved requestId and trace
values. See Server for the context blueprint and service
contexts.
When the context blueprint declares a gate, the server attaches ctx.gate
itself and re-attaches it after hooks enrich the context, so authorization
always evaluates the current identity.
Hooks run around the handler for application-level behavior: authentication at
the HTTP boundary, CORS, logging and tracing, rate limiting, response shaping,
and error mapping. Hooks can short-circuit the request; short-circuit
responses are framework-owned unless the hook returns a native Response.
See Hooks for the lifecycle order and hook kinds.
Route execution
After validation, the route runs. Binder routes ({ contract, useCase }) map
the validated request parts to the use case input, run the use case, and
return its output with the contract's declared success status:
{
contract: getProject,
useCase: getProjectUseCase,
}When the route shares schemas by reference with its use case, Beignet skips the redundant second validation pass. See Server for the binder's input mapping and one-schema-one-parse rules.
Full handle routes receive the validated parts directly and own the
response:
{
contract: getProject,
handle: async ({ req, ctx, path, query, headers, body }) => ({
status: 200,
body: await getProjectUseCase.run({ ctx, input: { id: path.id } }),
}),
}Response ownership
Beignet separates responses into three groups:
| Owner | Source | Validation |
|---|---|---|
| Route-owned | Bound use case output, handler { status, body }, or route-owned AppError | Validated against contract.responses |
| Framework-owned | Parsing, validation, hooks, unmatched routes, global error handling, contract violations | Uses Beignet's standard error envelope |
| Transport-owned | Native Response returned by handler or hook | Bypasses JSON response validation |
Route-owned responses are business outcomes: responses returned by the
handler, and AppError instances thrown by the handler or bound use case.
When the route declares response schemas, an undeclared status or a
non-matching body becomes a framework-owned 500 contract-violation response;
an empty contract.responses applies no response validation. The
validateResponses server option and its production posture live in
Server.
Framework-owned responses skip route response validation and use Beignet's standard error envelope when applicable:
- Request parsing and request validation errors
- Unmatched route 404s and method-mismatch 405s
- Hook short-circuit responses from
onRequestorbeforeHandle mapUnhandledErrorresponses- Internal contract-violation 500s
Framework-owned Beignet error envelopes include
x-beignet-error-owner: framework so generated clients can distinguish
framework errors from route-owned error responses that share the same status
code.
Transport-owned responses are native Response objects returned from handlers
or hooks. They bypass JSON response validation, and beforeSend sees a
headers-only view where only header changes apply; afterSend still observes
their status and headers. Use them for non-JSON payloads, redirects,
streaming, and Server-Sent Events.
This keeps the contract strict for business responses while letting infrastructure concerns such as auth, CORS, rate limits, malformed requests, and unexpected failures stay outside each route's response union.
Client behavior
Typed clients use the same contracts. A non-2xx route-owned response is parsed
against the declared error response schema. Framework-owned errors use
Beignet's standard { code, message, details?, requestId? } envelope when the
response includes Beignet's ownership header.
Use call() when failures should throw ContractError. Use safeCall() when
explicit result branching is clearer. See Client for
narrowing and handling patterns.