App architecture
A Beignet app keeps production code in a small set of predictable places, and
each folder owns one kind of decision. This page is the map: what lives where,
where each production concern goes, and the dependency direction
beignet lint enforces. For the first-hour loop, use
Quickstart; for the guided tour of one feature, use
Build your first feature.
What goes where
| Path | Responsibility |
|---|---|
features/<feature>/contracts.ts | HTTP surface: method, path, params, request body, headers, responses, metadata, and catalog errors |
features/<feature>/schemas.ts | Shared DTO and validation schemas that contracts, use cases, ports, client modules, and tests may import |
features/<feature>/routes.ts | Feature route group that maps contracts to use cases |
features/<feature>/use-cases/ | Application workflows with input and output validation |
features/<feature>/domain/ | Feature-owned entities, value objects, and domain events |
features/<feature>/components/ | Feature-owned UI and client workflows |
features/<feature>/policy.ts | Feature-owned authorization rules |
features/<feature>/ports.ts | Feature-specific dependency interfaces such as repositories |
features/<feature>/notifications/, uploads/, jobs/, listeners/, schedules/, tasks/, seeds/ | Feature-owned workflow artifacts, added by generators when needed |
features/<feature>/tests/ | Feature behavior tests, with shared factories in tests/factories/ |
features/shared/errors.ts | Application error catalog and route-owned error schemas |
features/shared/domain/ | Shared-kernel domain concepts used across features |
server/routes.ts | Central route registry and OpenAPI contract list |
server/context.ts | Shared context blueprint reused by the runtime server and route tests |
server/index.ts | Runtime wiring: context, hooks, providers, and error mapping |
server/providers.ts | Beignet lifecycle providers installed at server startup |
server/tasks.ts, server/outbox.ts, server/schedules.ts | App-owned registries and CLI contexts for tasks, outbox draining, and schedules |
app/api/ | Thin Next.js route files that expose server.api |
app-context.ts | Shared request context type used by handlers, hooks, and use cases |
ports/ | App-wide dependency interfaces shared across features |
infra/ | Concrete adapters and default port wiring, including infra/app-ports.ts |
lib/ | Small app helpers: env.ts, auth.ts, and the use-case.ts builder |
client/ | Typed Beignet client and frontend adapter factories |
The CLI starter scaffolds the subset of this structure it ships: contracts,
schemas, routes, use cases, components, ports, infra, server composition, and
the client. Workflow-tier artifacts such as jobs, listeners, notifications,
schedules, uploads, the outbox registry, and operational tasks appear when
beignet make generators add them, along with their lib/ builders and
server registries. beignet doctor treats their absence as fine and their
misplacement as drift.
Production concern map
When you know what you need to build, this table says where it goes:
| Concern | Put it here | Read next |
|---|---|---|
| Endpoint shape | features/<feature>/contracts.ts | Contracts |
| Feature route wiring | features/<feature>/routes.ts | Server |
| Request routing | server/routes.ts, server/index.ts, and app/api/ | Routes and server |
| Request lifecycle behavior | server hooks | Request lifecycle, Hooks |
| Business workflow | features/<feature>/use-cases/ | Use cases |
| Business authorization | features/<feature>/policy.ts or app-owned policy helpers | Authorization |
| Persistence and transactions | feature repository ports plus ctx.ports.uow.transaction(...) | Database and transactions |
| Audit/activity logging | ctx.ports.audit plus request actor, tenant, and requestId | Audit and activity logging |
| Cached reads and invalidation | ctx.ports.cache from infra/ or a cache provider | Cache |
| Object storage | ctx.ports.storage from infra/ or a storage provider | Storage |
| Uploads | features/<feature>/uploads/, StoragePort, and app-owned attachment records | Uploads |
| Domain events | features/<feature>/domain/events/, feature listeners, Unit of Work event recorder | Events |
| Background work | ctx.ports.jobs and job definitions | Jobs |
| Scheduled work | features/<feature>/schedules/ and a cron/provider trigger | Schedules |
ctx.ports.mailer and mail provider adapters | ||
| Notifications | features/<feature>/notifications/ and ctx.ports.notifications | Notifications |
| Structured logging | ctx.ports.logger and request logging hooks | Logging |
| Rate limiting | contract metadata plus rate limit hooks | Rate limiting |
| Provider startup and teardown | server/providers.ts | Providers |
| Env vars and deployment config | lib/env.ts | Config, Deployment |
| App errors | features/shared/errors.ts and contract .errors(...) | Errors |
| OpenAPI route | app/api/openapi/route.ts | OpenAPI |
| Dev-only request inspection | app/api/devtools/[[...path]]/route.ts | Devtools |
| UI data fetching | client/, features/<feature>/components/, React Query | React, React Query |
Dependency direction
The rule is simple: transport, application behavior, and infrastructure do not
own each other. A feature owns its contracts, route group, use cases, policy,
domain model, UI, and feature-specific ports. App-wide ports describe
dependencies shared across features. Infra implements those ports. Domain
concepts that are genuinely shared across features live in
features/shared/domain/.
beignet lint enforces the most important directions. Domain and use cases
cannot import infra, UI, route, client, provider, or framework code. Feature
domain cannot import another feature's domain unless it comes from
features/shared/domain. Route files cannot import infra or UI, and infra
adapters cannot import UI, routes, server modules, or clients. Contract files
and everything reachable from client/ or "use client" modules are also
checked as client-safe import graphs that must not reach server-only code; see
the CLI reference for the full lint rules.
Two placement notes that follow from the rule:
- Small feature-root helper modules are allowed — for example a
features/issues/history.tswith pure helpers shared across the feature's use cases. Keep them pure: as soon as a helper needs a dependency, give it a port. - Test placement follows ownership: feature behavior tests live in
features/<feature>/tests/, while infra and server modules may keep adjacent*.test.tsfiles beside the module they exercise, such as a repository test next to its adapter.
Feature-owned UI
Product UI lives with the feature it serves, in
features/<feature>/components/. Shared client wiring — the typed Beignet
client, React Query helper, React Hook Form helper, upload client, and
QueryClient provider — lives in client/. Feature components import those
helpers plus their feature's contracts, and call endpoints through React
Query and form adapters.
components/ may contain React Server Components and Client Components.
Server Components can call server-only modules when they are not reachable
from a client root. Client Components and anything they import cannot reach
use cases, route groups, infra adapters, server modules, provider packages,
or app-context.ts — keep server-only workflows behind route groups and
explicit server entrypoints. See React for the component patterns.
Server composition
Features keep route wiring local in features/<feature>/routes.ts, and the
server composes them at the boundary: server/routes.ts owns the central
route list, server/context.ts declares the context blueprint once for the
runtime and route tests, server/index.ts assembles ports, providers, hooks,
and routes, and a catch-all app/api/[[...path]]/route.ts exposes
server.api. That convention gives the CLI one source of truth for route
inspection, generation, OpenAPI wiring, and drift checks. The code for each
piece is on Routes and server.
Custom paths
Use beignet.config.ts when your app keeps the same architecture under
different paths:
import { defineConfig } from "@beignet/cli/config";
export default defineConfig({
paths: {
contracts: "src/features",
features: "src/features",
routes: "src/app/api",
server: "src/core/server/index.ts",
},
});Config changes where the CLI looks and writes. It does not replace the architecture: feature contracts still define the HTTP boundary, the server still registers route groups, and application code still belongs behind use cases and ports.