Concepts
Beignet is organized around a small set of boundaries. The goal is to define the shape of your API once, then reuse that definition across the server, client, validation, docs, and optional application architecture.
Read this page after Quickstart or App architecture. It explains the mental model behind the folders the starter creates.
Contracts
A contract describes one HTTP endpoint: method, path, path parameters, query parameters, request body, response statuses, response bodies, and metadata.
Contracts are the source of truth for:
- Server request validation
- Server route-owned response validation
- Client request and response types
- React Query keys and options
- React Hook Form body validation
- OpenAPI generation when schemas are Zod
Server runtime
The server runtime turns a contract into a route handler. Incoming data is parsed and validated before your handler runs. Handler responses are validated before they are sent when the contract declares response schemas.
Use the app error catalog for expected business failures, then declare those
catalog errors on the contract with .errors(...).
export const getTodo = todos
.get("/:id")
.responses({ 200: TodoSchema })
.errors({ TodoNotFound: errors.TodoNotFound });
export const GET = server.route(getTodo).handle(async ({ ctx, path }) => {
const todo = await ctx.ports.todos.findById(path.id);
if (!todo) {
throw appError("TodoNotFound", { details: { id: path.id } });
}
return { status: 200, body: todo };
});
Response ownership
Beignet separates responses into three groups:
- Route-owned responses are business responses returned by the handler or thrown as
AppErrorfrom the handler. They are validated againstcontract.responses. - Framework-owned responses come from request parsing, request validation, hooks, unmatched routes, global error handling, or contract violations. They skip route response validation and use Beignet's standard
{ code, message, details? }envelope when applicable. - Transport-owned responses are native
Responseobjects returned from handlers or hooks. They bypass JSON response validation andbeforeSend, and are useful for non-JSON responses, redirects, streaming, and Server-Sent Events.
This keeps the contract strict for business outcomes without forcing every route to declare infrastructure responses such as auth failures, CORS preflights, malformed JSON, or unexpected failures.
Hooks
Hooks are ordered lifecycle functions for infrastructure behavior. Use them for concerns that should not live inside every route handler: auth, CORS, rate limits, logging, tracing, response shaping, and error mapping.
Hooks can short-circuit before the handler, enrich context, observe responses, or map errors.
For auth, treat hooks as the HTTP boundary and use cases as the business boundary. Hooks can load or require a session; use cases and app-owned policy functions should decide whether the signed-in user may perform the workflow.
Ports and providers
Ports are the dependency interface your app code uses through ctx.ports. Providers are production adapters that install or replace ports at server startup.
export const server = await createNextServer({
ports,
providers: [loggerPinoProvider, redisProvider],
createContext: ({ ports }) => ({ ports }),
});
Tests can pass mock ports directly, while production can install Redis, Pino, Better Auth, Turso, Inngest, mail providers, and other adapters. See Providers for lifecycle details and the production feature pages for task-specific setup.
Client calls
The client has two request styles:
call()returns the response body for 2xx responses and throwsContractErrorfor failures.safeCall()returns{ ok: true, data }or{ ok: false, error }.
Use call() with React Query and most UI data-fetching flows. Use safeCall() when explicit branching reads better than exceptions.
Application and domain
The application package defines reusable use cases with input/output validation. Use it when the same business operation should run from HTTP routes, jobs, scripts, tests, or event handlers.
The domain package provides optional helpers for value objects, entities, and domain event declarations. Use it when you want more structure around core business concepts.
Production features
Beignet apps organize production concerns by the same dependency boundary:
- Database and transactions use repository ports and Unit of Work.
- Cache keeps cached reads behind
ctx.ports.cache. - Storage keeps object storage behind
ctx.ports.storage. - Events model facts that happened and listeners that react.
- Jobs model explicit work and durable dispatch.
- Scheduled tasks model cron-triggered workflows.
- Mail sends email through
ctx.ports.mailer. - Authentication identifies the current user at the HTTP boundary.
- Authorization keeps business permission checks in use cases and policies.
- Rate limiting protects routes from metadata-driven hooks.
- Logging gives requests, use cases, jobs, schedules, and providers structured diagnostics.