The Frond
A Frond is a business module that groups the elements responsible for one domain.
The ownership boundary
A Frond contains:
- its entities and schemas,
- its operations — commands and queries (handlers),
- its policies — access rules checked inside operations,
- its events — the facts its commands announce, which nothing subscribes to by default,
- its persistence adapters — how its entities are realized,
- its public contracts — what other Fronds may call,
- its invalidation rules — what a command revalidates.
A Frond does not import another Frond's internal files. Interactions go through explicit contracts, preferably operations or events. An App composes Fronds and keeps business logic in those modules rather than in its pages, fetches, or stores.
The six layers
The model follows six steps:
| # | Layer | What happens |
|---|---|---|
| 1 | Declaration | the author declares entities, views, operations, events — no HTTP, no SQL here |
| 2 | Derivation | validation, types, io views, adapter-readable metadata derive from it |
| 3 | Local capability | the declaration becomes executable in-process — the reference mode |
| 4 | Projection | façades are generated toward transports: HTTP, GraphQL, CLI, workers |
| 5 | Composition | Fronds are scanned and linked in one runtime — module → contract → interaction |
| 6 | Distribution | a capability moves out of process; serialization, timeouts, retries appear here, explicitly |
The gradient page covers the move from local to remote execution.
Generated vs handwritten
The split of responsibilities is:
| Fougere generates | You write |
|---|---|
| validation, derived views, tables/metadata | non-trivial business logic |
| GraphQL types and inputs, trivial CRUD | rich invariants, multi-step workflows |
| projection façades (contract → transport) | error policies, consistency decisions |
| typed clients, composition manifests | sensitive inter-Frond orchestration |
Fougere handles repetitive integrations around a contract. The developer writes application-specific behavior. A service layer remains optional: a handler can contain the logic or delegate it.
In the file system
fronds/blog/
entities/ ← the declarations
handlers/ ← the operations (handlers/<surface>/ = a named audience)
presenters/ ← computed fields added to an entity's output
collectors/ ← parameter resolution by type
services/ ← plain classes, injected by type
repositories/ ← the same, under a second name
seeds/ ← data created at startup
versions/ ← what its shapes used to be (`fougere freeze`)
The scanner recognizes a Frond through these directories. The project root can follow
the same convention, so a single-domain app does not need a fronds/ directory
(the flat shape). A second domain can
later be added under fronds/ without moving the first one.
These names are the only place in Fougere where a name IS the declaration, so they are the
only ones a project can restate. conventions: in fougere.config.ts names what differs
and nothing else:
export default defineFougere({
conventions: {
scope: '@domains', // default: '@fronds'
fronds: 'domains', // default: 'fronds'
dirs: { entities: 'models' }, // the other seven keep their names
},
})
It is read before the fronds are discovered, which is what lets it name the scope they
are found under — so nothing in fougere.config.ts may import @fronds/*. Everything
downstream follows: the scan, the aliases a page resolves, the watcher, and the
package.json that fougere sync writes into a consumer.
Naming and importing a Frond
The directory name is the Frond's name, and @fougere/nuxt turns it into an alias:
fronds/blog/ gives @fronds/blog, so a page writes
import Post from '@fronds/blog/entities/Post' with no file to add. The name also keys the
entity registrations and any remotes: entry.
A package.json in the Frond answers the two questions the directory cannot.
{
"name": "@fronds/blog",
"fougere": { "frond": "blog" },
"exports": { "./entities/*": "./entities/*.ts" }
}
fougere.frond allows a name that differs from the directory. For example,
fronds/blog-v2/ can remain registered as blog, preserving its entity keys,
@fronds/* imports, and remotes: entries. Without this property, Fougere uses the
directory name.
name and exports make the Frond resolvable outside a Nuxt app — a plain Node
script, a non-Nuxt consumer — which also needs the Frond declared as a workspace package
(packages: [fronds/*] in pnpm-workspace.yaml). Inside a Nuxt app the alias already
covers it.
services/ and repositories/ are both scanned as providers, resolved by type. Every
entity already has a default repository: Crud(Entity) receives it automatically, and
other code asks for RepositoryOf<Entity> or a declared <Entity>Repository. Direct
Storage injection into a handler, presenter or collector is refused at boot. Write a
repository when a query deserves a name or several entities form an aggregate; write a
service for a source that is not storage at all (an external API or a search index).
When one of those classes extends another, the base becomes a
port: the handler declares the base and receives the
implementation, and which one answers is a line in fougere.config.ts — never in the
Frond.