Skip to content
NestJS ns fundamentals 4 min read

Modules

Modules are the organizational backbone of every NestJS application. A module is a class annotated with @Module() that groups a closely related set of capabilities — controllers, providers, and the dependencies they need — into a single cohesive unit. Nest uses these modules to build an internal dependency graph that wires your whole application together, so understanding modules is the key to keeping a growing codebase maintainable.

The @Module decorator

The @Module() decorator takes a single object whose properties describe everything Nest needs to know about that slice of your application. There are exactly four metadata keys, and each plays a distinct role.

PropertyPurpose
controllersControllers instantiated by Nest that handle incoming requests for this module.
providersServices, repositories, factories, and other injectables managed by the Nest IoC container.
importsOther modules whose exported providers this module needs to consume.
exportsThe subset of this module’s providers that should be visible to modules that import it.

A typical feature module ties these together around one domain concept:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

Here CatsModule registers a controller, declares CatsService as a provider so Nest can inject it, and exports that service so other modules can reuse it.

Module encapsulation

A core principle in Nest is that modules encapsulate their providers. A provider declared inside a module is private to that module by default. If another module wants to inject CatsService, it cannot simply list it in its own providers — it must import the module that owns and exports it.

import { Module } from '@nestjs/common';
import { CatsModule } from '../cats/cats.module';
import { ShelterService } from './shelter.service';

@Module({
  imports: [CatsModule],
  providers: [ShelterService],
})
export class ShelterModule {}

Because CatsModule exports CatsService, ShelterService can now inject it through normal constructor injection:

import { Injectable } from '@nestjs/common';
import { CatsService } from '../cats/cats.service';

@Injectable()
export class ShelterService {
  constructor(private readonly catsService: CatsService) {}

  countCats(): number {
    return this.catsService.findAll().length;
  }
}

If you try to inject a provider that its owning module does not export, Nest fails fast at bootstrap with a clear Nest can't resolve dependencies error — encapsulation is enforced, not advisory.

The module graph

Every Nest application is a graph of modules connected through their imports. When you call NestFactory.create(), Nest walks this graph starting from the root module, instantiates each module’s providers in dependency order, and resolves every injection point. Providers are singletons within their module scope by default and shared across the modules that import them, so the same CatsService instance is reused everywhere it is consumed.

This graph is what gives Nest applications their predictable structure: each edge is an explicit import, so the flow of dependencies is always visible in code rather than hidden behind global state.

The root module pattern

Every application has exactly one root module — conventionally named AppModule — that Nest uses as the entry point to build the graph. The root module rarely contains business logic itself; instead it composes the feature modules that do.

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
import { ShelterModule } from './shelter/shelter.module';
import { AppController } from './app.controller';

@Module({
  imports: [CatsModule, ShelterModule],
  controllers: [AppController],
})
export class AppModule {}

The application is then bootstrapped from this root module:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

You can scaffold new feature modules with the Nest CLI, which generates the module file and automatically registers it in the closest parent module:

nest generate module cats

Output:

CREATE src/cats/cats.module.ts (82 bytes)
UPDATE src/app.module.ts (312 bytes)

Re-exporting modules

A module can export not only its own providers but also entire modules it imports. This lets you build a single “barrel” module that aggregates several others and exposes them through one import.

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
import { ShelterModule } from './shelter/shelter.module';

@Module({
  imports: [CatsModule, ShelterModule],
  exports: [CatsModule, ShelterModule],
})
export class AnimalsModule {}

Any module that imports AnimalsModule now gains access to everything CatsModule and ShelterModule export, without listing each one individually.

Best Practices

  • Organize one module per feature or bounded domain, keeping its controllers and providers tightly scoped.
  • Only export providers that genuinely need to be shared — keep internal helpers private to preserve encapsulation.
  • Keep the root AppModule thin: use it to compose feature modules, not to hold business logic.
  • Prefer explicit imports/exports over global modules so dependencies stay traceable.
  • Use the Nest CLI (nest g module <name>) so wiring into the parent module stays consistent.
  • Re-export aggregator modules sparingly to avoid hiding which provider actually owns a dependency.
Last updated June 14, 2026
Was this helpful?