Hooks
Hooks are ordered lifecycle functions for infrastructure behavior around route handlers. Use them for auth, CORS, logging, tracing, rate limits, response shaping, and error mapping.
Use hooks for HTTP boundary authentication and infrastructure checks: parse the
session, reject routes that require a signed-in request, or enrich ctx. Keep
business authorization in use cases or app-owned policy functions so the same
ownership, role, tenant, or resource-state rule runs outside HTTP too.
For the full auth story, read Authentication and Authorization. For production observability and traffic protection, read Logging and Rate limiting.
Hooks are configured on the server:
export const server = await createNextServer({
ports,
hooks: [loggingHooks, authHooks, rateLimitHooks],
createContext: ({ ports }) => ({ ports }),
});
Common infrastructure concerns should use first-party hook helpers when they fit:
import {
createAuthHooks,
createCorsHooks,
createLoggingHooks,
createRateLimitHooks,
} from "@beignet/core/server";
const hooks = [
createCorsHooks({ origins: "*" }),
createLoggingHooks({ logger }),
createAuthHooks<AppContext>({
assign: ({ ctx, session }) => ({
...ctx,
auth: session,
user: session?.user ?? null,
}),
}),
createRateLimitHooks<AppContext>(),
];
Lifecycle
Hooks run in this order:
onRequestruns after route matching and before request parsing orcreateContext.- Request path, query, headers, and body are parsed and validated.
createContextruns.beforeHandleruns before the route handler.- The route handler runs.
beforeSendcan shape the final response.afterSendobserves completion.onCaughtErrorobserves caught failures.mapUnhandledErrormaps unknown or otherwise unhandled failures.
onRequest
Use onRequest for raw request concerns that do not need parsed input or context.
const corsHooks = {
name: "cors",
onRequest: ({ req }) => {
if (req.method === "OPTIONS") {
return {
status: 204,
headers: {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET,POST,PATCH,DELETE,OPTIONS",
},
};
}
},
};
beforeHandle
Use beforeHandle when you need validated input, parsed contract headers, context, ports, or route metadata.
const tenantHooks = {
name: "tenant",
beforeHandle: async ({ ctx, headers, contract }) => {
if (contract.metadata?.tenant !== "required") {
return;
}
const tenant = await ctx.ports.tenants.findById(headers["x-tenant-id"]);
if (!tenant) {
return {
ctx,
response: {
status: 404,
body: { code: "TENANT_NOT_FOUND", message: "Tenant not found" },
},
};
}
return {
ctx: { ...ctx, tenant },
};
},
};
beforeHandle can return a new ctx, a short-circuit response, or both.
Metadata-driven hooks
Contracts can carry metadata for hooks.
export const createTodo = todos
.post("/api/todos")
.meta({
auth: "required",
rateLimit: { max: 10, windowSec: 60 },
})
.body(CreateTodoSchema)
.responses({ 201: TodoSchema });
const rateLimitHooks = {
name: "rate-limit",
beforeHandle: async ({ ctx, contract }) => {
const rule = contract.metadata?.rateLimit;
if (!rule) return;
const result = await ctx.ports.rateLimit.hit({
key: `route:${contract.name}`,
limit: rule.max,
windowSec: rule.windowSec,
});
if (!result.allowed) {
return {
ctx,
response: {
status: 429,
body: { code: "RATE_LIMITED", message: "Too many requests" },
},
};
}
},
};
beforeSend and afterSend
Use beforeSend to add headers or shape framework-owned responses. Use afterSend for logging, metrics, and tracing.
const loggingHooks = {
name: "logging",
beforeSend: ({ response }) => ({
...response,
headers: {
...response.headers,
"x-beignet": "1",
},
}),
afterSend: ({ req, response, durationMs }) => {
console.info(req.method, response.status, durationMs);
},
};
Error handling
Hook-thrown errors, handler-thrown unknown errors, and handler-thrown AppError instances are passed to onCaughtError observers. AppError instances are auto-mapped before mapUnhandledError; mapUnhandledError only maps unknown or otherwise unhandled failures.
const errorHooks = {
name: "errors",
onCaughtError: ({ err }) => {
console.error(err);
},
mapUnhandledError: ({ err }) => {
return {
status: 500,
body: {
code: "INTERNAL_SERVER_ERROR",
message: "Internal server error",
},
};
},
};
Hook short-circuit responses and mapUnhandledError responses are framework-owned, so they skip route response validation. Framework-owned Beignet error envelopes include x-beignet-error-owner: framework.