Modules

@RoostModule: cross-feature wiring and folder-wide chains, with encapsulation enforced at build time over a flat container.

A feature folder is already a roost module. Most apps never need more than that. When you do want cross-feature wiring or a chain that applies to a whole folder, drop an optional *.module.ts and decorate a class with @RoostModule.

// server/features/projects/projects.module.ts
@RoostModule({
  guards: [MemberGuard], // applies to every route in projects/
  exports: [ProjectsService], // the module's public surface
})
export class ProjectsModule {}

It's named @RoostModule (not @Module) on purpose — a roost module lives in server/features/** and is a different layer from a Nuxt module or layer.

What it declares

FieldEffect
importsother modules whose exported providers this module may depend on
exportswhich of this module's providers are visible to importing modules
guards / interceptors / filterschains applied to every route in the folder, between the global and class levels

There is intentionally no providers/controllers list — the folder is the discovery unit, so nothing is hand-maintained — and no forRoot/dynamic form (config lives in nuxt.config).

Module-scoped chains

A chain on the module applies to every route in the folder, so you declare it once instead of repeating an @UseGuards on each controller. The effective order is:

global → module → class → method

In manifest.json the module-contributed tokens are tagged (module), just as globals are tagged (global) — so the full effective chain stays visible on disk.

Encapsulation (enforced at build time)

By default any provider can be injected anywhere (the container is flat). A @RoostModule adds a boundary: a provider in another folder can depend on ProjectsService only if its module imports the projects module and the projects module exports ProjectsService. Otherwise the codegen throws:

[nuxt-roost] OrdersService depends on ProjectsService (module 'projects'),
but module 'orders' does not import ProjectsModule. Declare it via
@RoostModule({ imports: [...] }) on the consumer and @RoostModule({ exports: [...] }) on the owner.

_shared/ — and any _-prefixed folder — is implicitly global: its providers (the usual home for shared guards/filters) are visible everywhere with no import/export.

Encapsulation gates DI wiring, not name visibility. With autoImportFeatures every feature symbol is in one flat auto-import namespace, so a name is always referenceable; exports controls only what can be DI-resolved across a module boundary.

No runtime cost

The boundary is a compile-time check — the generated awilix container is byte-for-byte what it was without modules (no token namespacing, no per-module child containers). Modules are a build error, not a resolver feature.

One consequence of the global token space: two providers can't share a class name. roost reports that as a clear duplicate-name error rather than letting one silently shadow the other.

Adopt incrementally

Add one *.module.ts at a time. Folders without one keep working exactly as before — modules are opt-in.

Copyright © 2026