Modules

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against NestJS 12.1 (npm 12.1.2), TypeScript 6 (6.0.3), Node.js 20.19+ or 22.12+, Express 5 and Fastify 5, and was checked on 2026-10-02. The examples use a project generated by the Nest 12 CLI (ESM layout, Vitest, supertest), and the output shown is what they produce; the RouterModule routes behave the same on Fastify 5.

A module is a class decorated with @Module(). It is Nest’s unit of organization: it declares which controllers and providers belong together, which other modules it needs, and which of its providers other modules may use. Starting from the root module, Nest reads this metadata to build the application graph, the structure it uses to resolve every dependency (see Providers & Dependency Injection). A tiny application can live in a single module; every realistic one is a tree, or rather a graph, of modules. This page follows the running Bookshelf scenario and builds its module graph step by step. The language features used here (classes, decorators, the legacy decorator metadata) are explained in TypeScript for NestJS.

What a module is

Three facts explain most of Nest’s behaviour around modules:

  • A module is a class, and @Module() only attaches metadata to it. The class body is usually empty, although it may have a constructor that receives providers and lifecycle hooks (see A module class can inject providers).

  • Nest instantiates every module once. Importing the same module class from ten other modules does not create ten copies; this is why every module is already a shared module.

  • A module encapsulates its providers. A provider is visible inside the module that declares it and, from outside, only if the module exports it and the consumer imports that module.

The @Module() options

@Module() takes one object with four properties:

Property What goes in it Notes

providers

Providers instantiated by the Nest injector: classes, or custom providers (useValue, useClass, useFactory, useExisting).

Visible inside this module. Visible elsewhere only if also listed in exports.

controllers

Controllers this module owns. Nest instantiates them and registers their routes.

Controllers are never exported. They are reached over HTTP (or a message pattern), not by injection.

imports

Modules whose exported providers this module needs. May contain module classes, dynamic modules (the value returned by forRoot() and similar), and forwardRef(() ⇒ Module) references.

Importing gives access only to what the other module exports.

exports

The subset of this module’s providers that other modules may inject, written either as the provider class or as its token (the provide value). A module that this module imports may also be listed, which re-exports it.

An export is the module’s public interface. Exporting something that is neither a provider of this module nor an imported module fails at start-up with "Nest cannot export a provider/module that is not a part of the currently processed module".

Source: Modules.

There is no global key in the decorator: a module becomes global through the separate @Global() decorator or through the global property of a dynamic module (see Global modules and Introduction to dynamic modules). Because imports, providers and exports are plain arrays evaluated when the class is defined, you cannot reference a class declared later in the same file unless you wrap it in forwardRef().

The module class itself is the key of the module in the graph: Nest identifies a static module by its class, which is why the same class imported twice is the same module.

Feature modules

A feature module groups the code of one application domain: here, books. In Bookshelf the controller and the service of the catalogue live together in BooksModule:

books/books.service.ts
import { Injectable } from '@nestjs/common';

export interface Book {
  id: number;
  title: string;
  authorId: number;
}

@Injectable()
export class BooksService {
  private readonly books: Book[] = [
    { id: 1, title: 'Dune', authorId: 1 },
    { id: 2, title: 'Emma', authorId: 2 },
  ];

  findAll(): Book[] {
    return this.books;
  }

  findByAuthor(authorId: number): Book[] {
    return this.books.filter((b) => b.authorId === authorId);
  }
}
books/books.controller.ts
import { Controller, Get } from '@nestjs/common';
import { BooksService, type Book } from './books.service.js';

@Controller('books')
export class BooksController {
  constructor(private readonly books: BooksService) {}

  @Get()
  findAll(): Book[] {
    return this.books.findAll();
  }
}
books/books.module.ts
import { Module } from '@nestjs/common';
import { BooksController } from './books.controller.js';
import { BooksService } from './books.service.js';

@Module({
  controllers: [BooksController],
  providers: [BooksService],
  exports: [BooksService],
})
export class BooksModule {
  constructor(private readonly books: BooksService) {
    // A module class can inject providers of its own module (e.g. for set-up).
  }
}

BooksService is listed in exports because other features need it; BooksController is not exported because nothing injects a controller. Create the file skeleton with the CLI:

nest g module books
nest g controller books --no-spec
nest g service books --no-spec

The directory layout that results groups everything by feature rather than by technical role:

src/
  books/
    books.controller.ts
    books.module.ts
    books.service.ts
  authors/
  reviews/
  app.module.ts
  main.ts

Grouping by feature keeps boundaries visible: a new teammate can read one folder and one exports array to learn what a feature offers to the rest of the system. For how this scales to monorepos and libraries, see Project Structure & Workspaces.

Encapsulation

The rule that surprises newcomers most is that being in the same application is not enough for one provider to see another. A provider can be injected into a class only when one of the following holds:

  • the provider is declared in providers of the same module as the class that injects it;

  • the provider is declared in a module that the consumer’s module imports and that module lists it in exports (directly, by token, or by re-exporting a module that exports it);

  • the provider belongs to a global module (see Global modules).

Anything else produces the best-known Nest error. In the next example, HiddenCounterModule provides Counter but does not export it, and ConsumerModule imports the module and tries to inject Counter:

pitfalls.ts (encapsulation)
@Injectable()
export class Counter {
  value = 0;
}

// Counter is a provider of HiddenCounterModule but is NOT exported.
@Module({ providers: [Counter] })
export class HiddenCounterModule {}

@Injectable()
export class NeedsCounter {
  constructor(readonly counter: Counter) {}
}

@Module({ imports: [HiddenCounterModule], providers: [NeedsCounter] })
export class ConsumerModule {}

Compiling ConsumerModule (or calling NestFactory.create() with it in the graph) fails before the server starts:

Start-up error
Nest can't resolve dependencies of the NeedsCounter (?). Please make sure that the argument Counter at index [0] is available in the ConsumerModule module.

Potential solutions:
- Is ConsumerModule a valid NestJS module?
- If Counter is a provider, is it part of the current ConsumerModule?
- If Counter is exported from a separate @Module, is that module imported within ConsumerModule?
  @Module({
    imports: [ /* the Module containing Counter */ ]
  })

Source: Common errors and Modules.

The fix is the one the message suggests: add exports: [Counter] to HiddenCounterModule. The error always names the consumer module ("available in the ConsumerModule module"): that is the place to look first, then the module that should export the provider. Providers and modules fail at start-up, not at request time, so a successful boot already proves that the graph is consistent. The other frequent causes (injecting an interface, a circular import, a missing @Injectable()) are listed in Providers & Dependency Injection.

Shared modules and singletons

Modules are singletons, so every module is automatically a shared module: once BooksModule exports BooksService, every importer receives the same instance. AuthorsModule below lists a controller that uses BooksService without declaring it as a provider:

authors/authors.controller.ts and authors.module.ts
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { BooksService, type Book } from '../books/books.service.js';

@Controller('authors')
export class AuthorsController {
  // BooksService is visible because AuthorsModule imports BooksModule,
  // which exports it.
  constructor(private readonly books: BooksService) {}

  @Get(':id/books')
  booksOf(@Param('id', ParseIntPipe) id: number): Book[] {
    return this.books.findByAuthor(id);
  }
}

import { Module } from '@nestjs/common';
import { BooksModule } from '../books/books.module.js';
import { AuthorsController } from './authors.controller.js';

@Module({
  imports: [BooksModule],
  controllers: [AuthorsController],
})
export class AuthorsModule {}

Registering the provider in each module that needs it also compiles, but each module then gets its own instance: two caches, two counters, two connection pools. The following test-bed shows both layouts side by side. SharedA and SharedB import CounterModule, while CopyB declares Counter itself:

pitfalls.ts (one instance versus two)
@Module({ providers: [Counter], exports: [Counter] })
export class CounterModule {}

@Module({ imports: [CounterModule], providers: [FirstUser], exports: [FirstUser] })
export class SharedA {}
@Module({ imports: [CounterModule], providers: [SecondUser], exports: [SecondUser] })
export class SharedB {}
@Module({ providers: [Counter, SecondUser], exports: [SecondUser] })
export class CopyB {}

@Module({ imports: [SharedA, SharedB] })
export class SharedRoot {}   // FirstUser and SecondUser share one Counter
@Module({ imports: [SharedA, CopyB] })
export class CopyRoot {}     // FirstUser and SecondUser get different Counters

FirstUser.counter === SecondUser.counter is true for SharedRoot and false for CopyRoot. Whenever a provider holds state or an expensive resource, give it exactly one host module and export it from there.

Re-exporting modules and exporting by token

A module can export modules it imports, so that its importers get the whole bundle without importing each piece. CoreModule below imports a global configuration module and a clock module, and re-exports only the clock:

core/clock.module.ts and core/core.module.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class ClockService {
  now(): Date {
    return new Date('2026-10-02T00:00:00Z');
  }
}

import { Module } from '@nestjs/common';
import { ClockService } from './clock.service.js';

@Module({ providers: [ClockService], exports: [ClockService] })
export class ClockModule {}

import { Module } from '@nestjs/common';
import { ClockModule } from './clock.module.js';
import { AppConfigModule } from './config.module.js';

// Re-exports ClockModule: importing CoreModule is enough to inject ClockService.
@Module({
  imports: [AppConfigModule, ClockModule],
  exports: [ClockModule],
})
export class CoreModule {}

Importing CoreModule is enough to inject ClockService. AppConfigModule is imported by CoreModule but not re-exported, so it is not visible through CoreModule (it is available in this example only because it is global).

exports also accepts a provider’s token. A string or Symbol token is the usual choice for providers that are not classes (see Providers & Dependency Injection):

pitfalls.ts (export by token)
export const MAILER = Symbol('MAILER');

@Module({
  providers: [{ provide: MAILER, useValue: { send: () => 'sent' } }],
  exports: [MAILER],
})
export class MailModule {}

Source: Modules and Custom providers.

A consumer then writes @Inject(MAILER). Re-exporting is a good fit for a "core" or "platform" module that bundles infrastructure (logging, clock, configuration) behind one import. It is a poor fit for hiding which feature a provider really comes from: keep re-export chains short.

A module class can inject providers

The module class is itself instantiated by the container, so its constructor can receive providers of its own module (or imported ones), for example to perform set-up. BooksModule above does this with BooksService. The reverse is not possible: a module class cannot be injected as a provider, and Nest documents the reason as the risk of circular dependencies. Modules may also implement lifecycle hooks such as onModuleInit() and the NestModule interface, whose configure() method is where middleware is applied:

books/books-with-middleware.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import type { Request, Response, NextFunction } from 'express';
import { BooksController } from './books.controller.js';
import { BooksService } from './books.service.js';

function stamp(_req: Request, res: Response, next: NextFunction) {
  res.setHeader('x-handled-by', 'books-module');
  next();
}

@Module({ controllers: [BooksController], providers: [BooksService] })
export class BooksWithMiddlewareModule implements NestModule {
  // Middleware is configured on the module class, not in @Module().
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(stamp).forRoutes(BooksController);
  }
}

Calling GET /books returned the header x-handled-by: books-module. Middleware is therefore configured on the module class, not in the @Module() object; its full API is covered in Middleware.

Global modules

Importing the same helper module in every feature is repetitive. @Global() makes a module’s exported providers injectable everywhere without being listed in any imports:

core/config.module.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppConfigService {
  readonly pageSize = Number(process.env.PAGE_SIZE ?? 20);
}

import { Global, Module } from '@nestjs/common';
import { AppConfigService } from './app-config.service.js';

@Global()
@Module({
  providers: [AppConfigService],
  exports: [AppConfigService],
})
export class AppConfigModule {}

ReviewsService injects AppConfigService although ReviewsModule never imports AppConfigModule:

reviews/reviews.service.ts
import { Injectable } from '@nestjs/common';
import { BooksService } from '../books/books.service.js';
import { AppConfigService } from '../core/app-config.service.js';
import { ClockService } from '../core/clock.service.js';

@Injectable()
export class ReviewsService {
  constructor(
    private readonly books: BooksService,
    private readonly config: AppConfigService, // global module: no import needed
    private readonly clock: ClockService, // re-exported through CoreModule
  ) {}

  summary() {
    return {
      books: this.books.findAll().length,
      pageSize: this.config.pageSize,
      at: this.clock.now().toISOString(),
    };
  }
}

Use the rules the official documentation gives:

  • Register a global module once, typically in the root or a core module. Importing it again elsewhere is redundant, not harmful, but it blurs where it comes from.

  • A global module must still export what it wants to share. @Global() removes the need to import the module, not the need to export the providers.

  • @Global() only affects visibility of providers. Controllers, the module’s own configure() method and its imports work as for any module.

The documentation states plainly that making everything global is not a good design decision. Over-using @Global() has concrete costs:

  • The imports array stops documenting dependencies, so you cannot tell what a module needs by reading it.

  • Every module can now depend on every global provider, which is the loose coupling the module system exists to prevent.

  • Unit-testing a module in isolation needs the global modules added to the testing module as well.

Good candidates are cross-cutting infrastructure used almost everywhere (configuration, a logger, a database connection handle). Domain providers such as BooksService should be imported explicitly. Libraries commonly give the consumer the choice with an isGlobal option; see Introduction to dynamic modules.

Introduction to dynamic modules

A static module is fixed at the point of its @Module() decoration. A dynamic module is configured by the importer: a static method, by convention forRoot(), register() or forFeature(), returns a DynamicModule, an object with a module property and further module metadata. Bookshelf’s feature flags work this way:

feature-flags/feature-flags.constants.ts, feature-flags.service.ts and feature-flags.module.ts
export const FEATURE_FLAGS = 'FEATURE_FLAGS';

import { Inject, Injectable } from '@nestjs/common';
import { FEATURE_FLAGS } from './feature-flags.constants.js';

@Injectable()
export class FeatureFlagsService {
  constructor(
    @Inject(FEATURE_FLAGS) private readonly flags: Record<string, boolean>,
  ) {}

  isEnabled(flag: string): boolean {
    return this.flags[flag] ?? false;
  }
}

import { DynamicModule, Module } from '@nestjs/common';
import { FEATURE_FLAGS } from './feature-flags.constants.js';
import { FeatureFlagsService } from './feature-flags.service.js';

@Module({
  providers: [FeatureFlagsService],
  exports: [FeatureFlagsService],
})
export class FeatureFlagsModule {
  static forRoot(
    flags: Record<string, boolean>,
    isGlobal = false,
  ): DynamicModule {
    return {
      module: FeatureFlagsModule,
      global: isGlobal,
      providers: [{ provide: FEATURE_FLAGS, useValue: flags }],
    };
  }
}

Key properties of what forRoot() returns:

  • The properties extend the static @Module() metadata; they do not replace it. The resulting module has FeatureFlagsService (static) and the FEATURE_FLAGS provider (dynamic).

  • The returned object may also carry imports, controllers, exports and global. global: true registers the module in the global scope without @Global(), which is what forRoot({ …​ }, true) does above.

  • forRoot() may return the module synchronously or as a Promise.

  • Each call creates a configuration of its own. The next example imports the module twice with different flags and injects the service into one provider of each importing module; the two instances are different objects (isEnabled('a') is true in one and false in the other). Configure the module once and share it by re-exporting the module class from a core module, without calling forRoot() again. Two forRoot() calls with equal-looking but separately created options objects are not collapsed into one instance either, so do not rely on Nest to deduplicate them.

Re-exporting a configured dynamic module
@Module({
  imports: [FeatureFlagsModule.forRoot({ a: true })],
  exports: [FeatureFlagsModule],
})
export class Core {}

Two modules importing Core (each with a provider that injects FeatureFlagsService) receive the same instance (the identity check is true).

The conventional method names are forRoot() (configure once, application-wide), register() (configure per importing module) and forFeature() (add feature-specific parts to an already configured root), each with an Async variant such as forRootAsync() that takes useFactory, inject or useClass so that options can come from ConfigService. Building them with ConfigurableModuleBuilder is the subject of the dynamic modules chapter of the official documentation (Dynamic modules), and the section’s index lists the page that covers it in full.

Routing prefixes with RouterModule

The URL of a handler is the controller prefix plus the method path, optionally preceded by a global prefix and a version. RouterModule (exported by @nestjs/core) adds one more level: a prefix for every controller of a module, declared in one place, with children for hierarchies. It applies to HTTP applications only.

Bookshelf’s administration area is made of an AdminModule that imports a DashboardModule and a MetricsModule:

admin/admin.controllers.ts
import { Controller, Get } from '@nestjs/common';
import { FeatureFlagsService } from '../feature-flags/feature-flags.service.js';

@Controller('stats')
export class DashboardController {
  constructor(private readonly flags: FeatureFlagsService) {}

  @Get()
  stats() {
    return { betaDashboard: this.flags.isEnabled('betaDashboard') };
  }
}

@Controller('metrics')
export class MetricsController {
  @Get()
  metrics() {
    return { requests: 0 };
  }
}
admin/admin.modules.ts
import { Module } from '@nestjs/common';
import { DashboardController, MetricsController } from './admin.controllers.js';

@Module({ controllers: [DashboardController] })
export class DashboardModule {}

@Module({ controllers: [MetricsController] })
export class MetricsModule {}

@Module({ imports: [DashboardModule, MetricsModule] })
export class AdminModule {}

and the application’s root module puts everything together, including the route tree (this is the module the Bookshelf graph below is drawn from):

app.module.ts
import { Module } from '@nestjs/common';
import { RouterModule } from '@nestjs/core';
import { AdminModule, DashboardModule, MetricsModule } from './admin/admin.modules.js';
import { AuthorsModule } from './authors/authors.module.js';
import { BooksModule } from './books/books.module.js';
import { AppConfigModule } from './core/config.module.js';
import { FeatureFlagsModule } from './feature-flags/feature-flags.module.js';
import { ReviewsModule } from './reviews/reviews.module.js';

@Module({
  imports: [
    AppConfigModule,
    FeatureFlagsModule.forRoot({ betaDashboard: true }, true),
    BooksModule,
    AuthorsModule,
    ReviewsModule,
    AdminModule,
    RouterModule.register([
      {
        path: 'admin',
        module: AdminModule,
        children: [
          { path: 'dashboard', module: DashboardModule },
          { path: 'metrics', module: MetricsModule },
        ],
      },
    ]),
  ],
})
export class BookshelfModule {}

Source: Router module.

The controllers keep their short prefixes (stats, metrics) and the module tree supplies the rest. Requests against the running application return:

Requests against the Bookshelf module graph
curl -s localhost:3000/books
curl -s localhost:3000/authors/1/books
curl -s localhost:3000/reviews/summary
curl -s localhost:3000/admin/dashboard/stats
curl -s localhost:3000/admin/metrics/metrics
curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/stats
[{"id":1,"title":"Dune","authorId":1},{"id":2,"title":"Emma","authorId":2}]
[{"id":1,"title":"Dune","authorId":1}]
{"books":2,"pageSize":20,"at":"2026-10-02T00:00:00.000Z"}
{"betaDashboard":true}
{"requests":0}

The un-prefixed /stats returned 404: once a module is registered in RouterModule, its controllers are reachable only under the module’s path. The same requests behave identically on Fastify (checked with app.inject() against FastifyAdapter), because route registration is handled by Nest core before the platform adapter sees the routes.

Things to know when using RouterModule:

  • The path of a child is appended to its parent’s path, recursively, from the top down.

  • RouterModule.register() only prefixes modules; it does not import them. In the example every module is also in an imports array. Registering a path for a module that nothing imports does not raise an error: the application starts and the route simply returns 404.

  • It combines with a global prefix: with app.setGlobalPrefix('api') the metrics route is served at /api/admin/metrics/metrics. See Controllers & Routing for prefixes, versioning and path syntax.

  • The official documentation advises using it carefully: a controller’s real URL is now spread over a decorator and a distant module tree, which makes routes harder to find.

Circular module imports

If module A imports module B and B imports A, Nest cannot decide which to build first. Both sides must use forwardRef(), in imports and (for injected providers) in @Inject(). In an ESM project, the constructor parameter type must also be wrapped so that TypeScript does not emit a reference to a class that is still uninitialized; the Relation<T> alias below does that (see TypeScript for NestJS):

pitfalls.ts (circular modules)
export type Relation<T> = T;

@Injectable()
export class AService {
  constructor(@Inject(forwardRef(() => BService)) readonly b: Relation<BService>) {}
}
@Injectable()
export class BService {
  constructor(@Inject(forwardRef(() => AService)) readonly a: Relation<AService>) {}
}
@Module({ imports: [forwardRef(() => BModule)], providers: [AService], exports: [AService] })
export class AModule {}
@Module({ imports: [forwardRef(() => AModule)], providers: [BService], exports: [BService] })
export class BModule {}

This compiles, starts and resolves AService.b.a. Treat forwardRef() as a repair, not a design tool: a cycle between two modules usually means that a third module should own what both need, or that one side should react to events instead of calling the other. The official page also warns against barrel files (index.ts) for module and provider classes because they create cycles of their own.

The Bookshelf module graph

Putting the pieces together, this is the module graph of the running example. Solid arrows are imports; the dashed arrow shows what the global modules provide implicitly.

flowchart TD App["BookshelfModule (root)"] Cfg["AppConfigModule
@Global()"] Flags["FeatureFlagsModule
forRoot(flags, global)"] Books["BooksModule
exports BooksService"] Authors["AuthorsModule"] Reviews["ReviewsModule"] Core["CoreModule
re-exports ClockModule"] Clock["ClockModule
exports ClockService"] Admin["AdminModule
/admin"] Dash["DashboardModule
/admin/dashboard"] Metrics["MetricsModule
/admin/metrics"] Router["RouterModule.register()
path prefixes only"] App --> Cfg App --> Flags App --> Books App --> Authors App --> Reviews App --> Admin App --> Router Authors --> Books Reviews --> Books Reviews --> Core Core --> Cfg Core --> Clock Admin --> Dash Admin --> Metrics Router -.->|prefixes| Admin Cfg -.->|AppConfigService visible everywhere| Reviews Flags -.->|FeatureFlagsService visible everywhere| Dash

Reading the graph from the root: the application imports its feature modules; AuthorsModule and ReviewsModule reach BooksService through BooksModule’s export; `ReviewsModule obtains ClockService through CoreModule’s re-export; and the two global modules supply `AppConfigService and FeatureFlagsService to modules that never import them. The later pages of the section keep growing this graph: the section index shows where each addition (database, authentication, queues) lands.

Nest can also draw this graph for you: setting snapshot: true in the NestFactory.create() options and installing @nestjs/devtools-integration lets the Devtools application display modules, providers and controllers as an interactive graph (see Devtools overview).

Where modules sit in the lifecycle

Modules act at start-up, before the first request:

  1. NestFactory.create(RootModule) scans the graph from the root, following imports (including dynamic modules and forwardRef`s), registers providers and controllers per module, and checks that each injected token is reachable under the encapsulation rules. A mistake in `imports or exports fails here.

  2. Module classes and providers are instantiated in dependency order.

  3. When the application is initialized (app.init() or app.listen()), onModuleInit() and onApplicationBootstrap() hooks run, and each module’s configure() registers its middleware. In Nest 12 hooks are called by component hierarchy level, which can change the order between related providers and modules compared with Nest 11; the migration guide asks you to review code that depends on a particular hook order.

  4. Requests flow through global middleware, then module middleware, and then guards, interceptors, pipes, the handler and filters; the module decides which middleware and enhancers apply to a controller, but it does not take part in per-request work itself. See Request Lifecycle.

Modules are independent of the HTTP platform: nothing on this page changes between Express and Fastify, except that module middleware written as raw Express functions (such as stamp above) needs the platform’s middleware API; on Fastify, Nest uses @fastify/middie, covered on the Middleware page. RouterModule works the same on both: the routes above answer identically on Fastify 5.

Pitfalls

Symptom Cause and remedy

Nest can’t resolve dependencies of the X (?)

The provider is not in the consumer’s module, not exported by the module that hosts it, or that module is not imported. Add exports and imports; see The @Module() options.

Two copies of a "singleton" service

The class is listed in providers of several modules. Give it one host module and export it.

@Global() used to avoid writing imports

Hides dependencies and couples modules. Keep it for configuration and logging; import everything else.

forRoot() called in several modules

Each call creates a separately configured instance. Call it once and re-export the module.

Controller routes return 404 after adding RouterModule

The module’s controllers now live under the registered path (/admin/dashboard/…​), or the module is not imported at all.

Start-up fails with a cycle between modules

Use forwardRef() on both sides (and a wrapped type in ESM), or break the cycle by extracting a shared module.

Exporting a provider the module does not own

exports takes the module’s own providers, its tokens and the modules it imports; anything else is rejected at start-up.

What changed recently, and where the book is outdated

  • Nest 12 changed one thing that touches modules at run time: lifecycle hooks (onModuleInit(), onApplicationBootstrap() and the shutdown hooks) are called by component hierarchy level, so the order between related providers and modules can differ from Nest 11. Review code that depends on a particular order.

  • Nest 12 projects are ESM by default in the CLI template: relative imports in module files carry a .js extension (see the examples above), and a module cycle needs a wrapped constructor type such as Relation<T>. A project may stay on CommonJS.

  • The official modules page now introduces dynamic modules with a FeatureFlagsModule.forRoot() example, shows re-exporting a configured dynamic module and the global: true property, and states that making everything global is not a recommended design. The examples here follow that structure.

  • RouterModule is documented as a separate recipe, not on the modules page; material that predates it describes prefixes only through @Controller() paths and setGlobalPrefix().

  • The book consulted for this section uses extension-less imports and CommonJS in its module examples, which do not compile unchanged in a project generated by the Nest 12 CLI, and it does not cover re-exporting a configured dynamic module or RouterModule children. Use the code on this page; the bibliography is on the section index.