---
name: routes-server
description: "Build Beignet Next.js server integration with @beignet/next: defineRouteGroup, defineRoutes, contractsFromRoutes, createNextServer, createNextClient, server context, hooks, OpenAPI and Swagger UI handlers, devtools routes, catch-all API adapters, uploads, storage routes, generic webhooks, payment webhooks, schedule cron routes, outbox drains, best-effort work, and route inspection. Use when wiring or debugging Beignet routes in a Next app."
---

# Beignet Next Routes And Server

Use this skill when working in a Next.js app that depends on `@beignet/next`.
Read local `AGENTS.md`, `beignet.config.*`, and existing route/server files
before changing code.

Use a maintained Next.js release: Next.js 15.5.24 or newer within v15, or
Next.js 16.3.8 or newer within v16.

## App-Bound Route Builders

Create the route declaration helpers once in `lib/routes.ts`:

```ts
import "@beignet/core/server-only";
import { createRoutes } from "@beignet/core/server";
import type { AppContext } from "@/app-context";

export const { defineRoute, defineRouteGroup } = createRoutes<AppContext>();
```

Feature route files import these helpers. Bind `AppContext` once in this module,
not separately in each feature.

## Route Shape

Feature route groups live beside the feature and map contracts to use cases.

```ts
import { defineRouteGroup } from "@/lib/routes";
import { createProject, getProject } from "@/features/projects/contracts";
import {
  createProjectUseCase,
  getProjectUseCase,
} from "@/features/projects/use-cases";
import { auth } from "@/lib/route-auth";

export const projectRoutes = defineRouteGroup({
  name: "projects",
  routes: [
    {
      contract: createProject,
      hooks: [auth.required()],
      useCase: createProjectUseCase,
    },
    {
      contract: getProject,
      useCase: getProjectUseCase,
      input: ({ path }) => ({ id: path.id }),
    },
  ],
});
```

Keep route entries thin: hooks, input mapping, policy checks, and use-case
binding. Business behavior belongs in use cases.

Without an `input` mapper, TypeScript checks the default binder result against
the use-case input. One request schema passes through unchanged; multiple
object sources merge with path over body over query precedence. Add an
explicit mapper when the shapes differ, validated headers contribute input, or
a scalar or array body is combined with another source.

## Central Registry

Register feature route groups once in `server/routes.ts`.

```ts
import { contractsFromRoutes, defineRoutes } from "@beignet/core/server";
import type { AppContext } from "@/app-context";
import { projectRoutes } from "@/features/projects/routes";

export const routes = defineRoutes<AppContext>([projectRoutes]);
export const contracts = contractsFromRoutes(routes);
```

OpenAPI, typed clients, devtools, and route inspection should derive from this
registry. If a route is missing from OpenAPI or clients, check this file first.

## Server Composition

`server/index.ts` owns runtime composition:

- `createNextServer(...)`
- `createNextServerLoader(...)`
- app ports
- providers
- hooks
- server context blueprint
- route registry
- response validation and error mapping

Keep app-specific infra wiring in `infra/` and `server/providers.ts`, not in
feature route files.

## Next Route Adapter

Application API routes usually use one catch-all file:

```ts
import { createApiRoute } from "@beignet/next";
import { getServer } from "@/server";

export const { DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT } =
  createApiRoute(getServer);
```

The catch-all file is adapter glue. Contracts should still use explicit paths
such as `/projects/:id`, not catch-all contract patterns. Keep provider
startup behind `getServer` so Next production builds can import route modules
without booting the app runtime.

## Focused Routes

Focused route files are appropriate for framework helpers and specialized
HTTP surfaces:

- OpenAPI or Swagger UI
- devtools
- uploads
- storage object routes
- payment or generic webhook adapters
- schedule cron routes
- outbox drains
- redirects or downloads

Use a route `handle` escape hatch only when the route intentionally returns a
native `Response`, streams, owns custom headers, or otherwise sits outside the
normal use-case binder.

The Next adapter shares `@beignet/web` response conversion. An explicit
`ReadableStream` in a plain Beignet response remains a stream for every media
type, including JSON; use a native `Response` when the endpoint needs full
transport ownership.

For Server-Sent Events, return `createServerSentEventResponse(...)` from
`@beignet/core/server` and pass `req.signal`. It owns framing,
heartbeats, bounded unread buffering, optional maximum lifetime, abort
handling, cleanup, and anti-buffering headers. Async subscription setup should
honor the stream-scoped `signal` passed to `start(...)`; an expected
`AbortError` caused by stream closure is cancellation rather than a reported
failure. Keep authorization, replay, connection limits, and reconciliation
application-owned.

Prefer adapter helpers for focused surfaces when the package provides them:
`createOpenAPIHandler(...)`, `createSwaggerUIHandler(...)`,
`createUploadRoute(...)`, `createStorageRoute(...)`,
`createWebhookRoute(...)`, `createPaymentWebhookRoute(...)`,
`createScheduleRoute(...)`, and `createOutboxDrainRoute(...)`.

OpenAPI documents enumerate protected routes as well as public ones, so add
app-owned authorization to spec and docs endpoints when that surface is
sensitive. `createSwaggerUIHandler(...)` loads version-pinned assets from
`unpkg.com`, safely serializes static or request-derived spec URLs at the HTML
script boundary, and emits inline script and style blocks; strict-CSP or
network-isolated deployments should use a custom self-hosted CSP-compatible UI.
Allowing the asset host alone is not sufficient.

For Next.js 15.1 and newer, apps may also use
`createNextOutboxDrainTrigger(...)` with `after()` and replace their database
Unit of Work from `server/providers.ts` with a
`createObservedUnitOfWork(...)` wrapper. This performs one push-assisted batch
after commit without creating an `infra -> server` dependency. Repeated and
nested triggers coalesce while that batch is scheduled or running. It is not
durable execution: keep
`createOutboxDrainRoute(...)` as a recovery sweep for missed callbacks,
delayed messages, retries, and additional batches.

For dispensable follow-up work on Next.js 15.1 or newer, define
`bestEffortWork: BestEffortWorkPort` in the app ports, defer the key in
`infra/port-wiring.ts`, and install
`createNextBestEffortWorkPort({ defer: after, onError })` from
`server/providers.ts`. The adapter isolates scheduler, callback, and observer
failures from the response. Use it only when losing the callback is acceptable;
required or retryable delivery belongs in a job or outbox. The Next adapter is
request-bound; workers and commands need a scheduler owned by their runtime.

The outbox registry does not install delivery transports. Its events require
`ctx.ports.eventBus`, and its jobs require `ctx.ports.jobs`; bind those ports
directly or defer them to startup providers. Missing transports reject the
route before a batch is claimed, so configuration failures do not consume
delivery attempts.

Schedule and outbox route helpers use an optional
`ctx.ports.errorReporter`. Schedule failures, dead-lettered messages,
lease failures, settlement failures, and drain infrastructure failures report
once; outbox retries remain instrumentation only. Reporter failures never
replace the route response. An outbox route returns 500 with the complete
result when `settlementFailed` or `leaseLost` is nonzero. Parallel drain
delivery is opt-in and does not preserve ordering.

Use `@beignet/webhooks-github` and `@beignet/webhooks-stripe` for vendor
signature verification. These are route-bound integrations, not lifecycle
providers, so do not register them in `server/providers.ts`. Focused helper
failures use Beignet's canonical `{ code, message }` operational error
envelope; do not add a second operational response shape.
Generic and signed payment webhook bodies are bounded to 1 MiB by default;
set `maxBodyBytes` only from the provider's documented limit. Payment routes
check the signature header first, return 400 for a missing signature, and
cancel the unread body before server resolution, hooks, or context creation.
Signed oversized bodies return 413 before those lifecycle phases too.

## Context

`server/context.ts` should use `defineServerContext`. Request context reads the
session, creates an actor, attaches ports, request IDs, trace context, and
optional `requestInfo`. Configure `trustedProxy` once on `createNextServer(...)`
when the deployment has a trusted edge; the context factory and server hooks
then receive the same external URL, origin, host, protocol, and optional client
IP. Forwarding headers remain untrusted by default, and apps choose whether to
persist client IPs in logs or audit metadata.
Service context is for schedules, tasks, outbox drains, and background work;
create a service actor and do not assume a request session. Focused helpers
such as `createOutboxDrainRoute(...)`, `createScheduleRoute(...)`, and webhook
routes build context through the server; keep their fulfillment behavior in
use cases or app service functions.

## Next Client

Shared browser client setup usually lives in root `client/`. Use
`createClient(...)` from `@beignet/core/client` when same-origin relative
requests are enough. Use `createNextClient(...)` from `@beignet/next` when
server-side calls need Next-friendly absolute URL defaults.

For Server Components and layouts, prefer an app-owned server-only context
helper (usually `lib/server-context.ts`) that wraps
`server.createContextFromNext()` in React `cache(...)`. Reading `ctx.auth`,
`ctx.tenant`, and request metadata from that context is appropriate for
redirects and shell state; feature data still belongs behind use cases.

For Server Component prefetching into React Query hydration, use the app's
server-only prefetch helper on top of that context for direct
`{ contract, useCase }` route bindings: keep `rq(contract).queryOptions(...)`
for the key and replace only the server `queryFn` with `useCase.run(...)`. Use
`createNextClient(...)` plus the HTTP `rq(...)` path when route handlers add
response mapping, headers, streaming, or other HTTP-layer behavior.

## Debugging

- Route group exists but does not run: register it in `server/routes.ts`.
- Contract missing from OpenAPI: export `contracts = contractsFromRoutes(routes)`.
- `doctor` dislikes the route shape: import the app-bound
  `defineRouteGroup({ ... })` from `lib/routes.ts`.
- Cron route fails closed: verify `CRON_SECRET` and the route's bearer token.
- Client code imports `server/`: move shared DTOs to schemas and call through
  contracts and the typed client.

Run:

```bash
beignet routes
beignet lint
beignet doctor --strict
```

## Broadcast routes

Mount `createBroadcastRoute` from `@beignet/next` in
`app/api/broadcasts/route.ts`, with a lazy `server: getServer` and the explicit
channel registry from `server/broadcasts.ts`. Reuse app authentication hooks;
hook-added context is inferred for channel authorization, including through
`server.rawRoute({ ..., hooks })`. Bindings must check current access on every
connection. `maxLifetimeMs` defaults to 60,000 ms and accepts positive safe
integers up to 3,600,000 ms (one hour). Use Node runtime and leave setup/cleanup
headroom below the host request deadline: for example `maxLifetimeMs: 240_000`
with `maxDuration = 300` when the platform permits five-minute requests.
Longer streams reduce renewal/refetch frequency and increase the interval
between authorization checks. The client uses the advertised lifetime plus
five seconds of watchdog grace for the whole connection, without extending it
on later readiness or heartbeats. Missing or invalid lifetime metadata blocks
subscriptions; heartbeat/readiness timeouts stay independent. Every renewal
still calls `onSync`.

Browser clients use streaming Fetch, multiplex channels, and refetch state on
readiness/reconnect. Serverless instances and separate workers require a shared
provider such as Redis; memory only reaches one process. The ioredis provider is
not an Edge adapter. Capture optional origin exclusion from authenticated
request context and forward it through publication jobs. See the broadcasting
concept guide before adding replay, presence, or offline guarantees.

Use [Broadcast coordination](./references/broadcast-coordination.md) for the
optional mutation refresh helper, custom write gates, connection admission,
and lifecycle metadata. Keep feature query mappings in feature `client/` code
and admission policy in `server/broadcast-admission.ts`, registered once on
the broadcast endpoint. The framework owns listeners and release callbacks.
