Layer Architecture
In blueprint: this page documents the presets'
architectureblock — the one part of the philosophy that compiles into hard gates, not just prose: the generated ESLint config andinspectfindings. Declare your own layers inblueprint.config.mjsand the same machinery enforces them.
One-way dependency flow + single responsibility per layer. That inner discipline is shared by both supported topologies:
- layer-first:
Layer → Unit; - module-first:
Module → Layer → Unitfor ordinary modules.
The canonical Vue and React presets are layer-first by default; given modules, the same presets project this doctrine onto module-first. Module-first adds an outer domain dependency graph; it does not replace the layer doctrine inside each ordinary module.
Why one-way
- Each layer keeps a single responsibility (a hook that never imports a component stays reusable logic)
- Ownership is visible at a glance — no repo-wide grep
- Refactors are safe: moving a file across layers makes lint flag every illegal call site
- Adding a dependency edge means editing the blueprint — "should this layer really depend on that one?" surfaces in review
The outer module boundary
In module-first, every ordinary module repeats the shared layer contract below. A module's dependsOn edges determine which other modules it can reach, including transitive dependencies. An import must satisfy both that outer graph and the inner one-way layer flow.
The optional reserved app module is different: it represents recursive router composition at the container position. It is not an ordinary domain module and does not repeat the shared layers. This exception keeps router ownership explicit without weakening the module/layer model for application features.
In a module-first preset, pages and containers below are positions rather than repeated layers: route composition belongs to the reserved app module, and each module's root files take the container position. The remaining layers repeat inside every ordinary module.
The layers
pages
- Does — page layout, assembling containers; routes, SEO
- Must not — hold business logic, stack components directly
containers
- Does — one feature: assembly, business logic, CRUD; stateful, calls services, drives navigation
components
- Does — presentation UI, including feature-private UI with a single consumer; may call hooks and own ephemeral presentation state
- Must not — touch the router, call services, own application or use-case state, orchestrate a use case
hooks
- Does —
inject/useContextlive only here; adapts server/shared state; stores (Pinia/Zustand) are private objects of this layer - Must not — expose a raw store
contexts
- Does —
provide/createContextlive only here; exposes Context/Provider
services
- Does — network primitives; the only importer of
axios, the only caller offetch/WebSocket - Must not — contain UI or business logic
There is no stores layer and no utils layer. A store has a single owner hook — its public face; other features read through that hook. And utils/ is a cohesion-free junk drawer that grows without bound until everything imports it — pure functions get homes by ownership instead: module-private files, or a named, domain-scoped module.
Ownership — owns
The "only importer of axios" cells above are not prose — they compile. A layer declares the primitives it exclusively owns, and every other layer is barred from them:
{ name: 'services', owns: ['axios', { global: 'fetch' }, { global: 'WebSocket' }] },
{ name: 'hooks', owns: [{ package: 'vue', imports: ['inject'] }, 'pinia'] },- a bare string owns a whole package;
{ package, imports }narrows it to specific named imports (vuestays importable everywhere — onlyinjectis fenced) { global }owns a global (fetch,WebSocket) — no import statement exists, so this half is enforced by lint (no-restricted-globals), notinspect- package ownership lands twice: lint (
no-restricted-imports) and inspect'spackage-ownershipfinding
Feature folder — one unit, one folder
components/
└─ Dropdown/
├─ index ← the only public entry
├─ Dropdown ← implementation, named after the unit (never "Component")
├─ hooks ← private
├─ styles ← private
└─ types ← privateindexis the unit's face — the outside world knows nothing else- Private sub-components may stay inside their owning unit (a container's
ProfileTab). A presentation unit may also live incomponents/with a single consumer. Reuse determines whether to promote it to a shared/public location, not its layer role - The implementation file carries the unit's name — a tab bar of ten
Component.tsxis unnavigable
components vs containers is a responsibility decision: "does it present UI, or own the use case?" A feature-specific display, such as a MatchScoreCard, remains a component even with one consumer. Its vocabulary and consumer count do not make it a container. A component may own ephemeral presentation state, such as an expanded section, hover state, or an open menu. Application state, workflow transitions, CRUD, and navigation belong to the container position. Containers wire components to data; components do not own the container's use case.
In module-first, a feature-private presenter lives under its owner's components layer, not at the module root, and needs no cross-module public export. In layer-first, components may retain feature ownership without a shared barrel export.
