Request lifecycle
A Beignet request moves through a predictable pipeline before your application code runs and before a response is sent.
HTTP request
-> adapter request normalization
-> route matching
-> request parsing
-> contract request validation
-> context creation
-> beforeHandle hooks
-> route handler or use case
-> route-owned response validation
-> beforeSend hooks
-> adapter response serialization
-> afterSend hooks
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.
If no route matches, Beignet returns a framework-owned 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.
Context and hooks
createContext builds the per-request context used by handlers, hooks, and use
cases. Typical context includes requestId, auth/session state, and ports.
Hooks run around the handler for application-level behavior:
- Authentication at the HTTP boundary
- CORS and headers
- Logging and tracing
- Rate limiting
- Response shaping
- Error mapping
Hooks can short-circuit the request. Short-circuit responses are
framework-owned unless the hook returns a native Response.
Handler execution
Handlers receive validated inputs:
{
req,
ctx,
path,
query,
body,
headers,
}
In Beignet apps, handlers usually call a use case and return a response that matches the contract:
{
contract: getProject,
handle: async ({ ctx, path }) => ({
status: 200,
body: await getProjectUseCase.run({
ctx,
input: { id: path.id },
}),
}),
}
Response ownership
Beignet separates responses into three groups:
| Owner | Source | Validation |
|---|---|---|
| Route-owned | Handler returns { status, body } or throws 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 |
Use route-owned responses for business outcomes. Use framework-owned responses
for infrastructure and lifecycle failures. Use native Response for redirects,
downloads, streaming, Server-Sent Events, or other non-JSON transport behavior.
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.