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:

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:

OwnerSourceValidation
Route-ownedBound use case output, handler { status, body }, or route-owned AppErrorValidated against contract.responses
Framework-ownedParsing, validation, hooks, unmatched routes, global error handling, contract violationsUses Beignet's standard error envelope
Transport-ownedNative Response returned by handler or hookBypasses 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:

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.