LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
Lesson 317 min read

Modules

Understand how NestJS modules organize an application into features, how imports and exports share providers, and when to use global and dynamic modules.

On this page 12 sections

Introduction

Every NestJS application is a tree of modules. The root module (AppModule) sits at the top, and each feature of your app — users, products, orders, authentication — lives in its own module underneath. Understanding modules is the key to understanding how Nest finds and connects everything else.

What You Will Learn
  • What a module is and why Nest uses them.
  • The four properties of the @Module() decorator.
  • How to create and register a feature module.
  • How to share a provider between modules with exports.
  • What global and dynamic modules are, and when to use them.

What is a Module?

A module is a class annotated with @Module() that groups a closely related set of capabilities. Think of it like a department in a company: the "Users" department has its own staff (providers) and its own front desk (controllers), and it can choose which staff it lends to other departments.

Nest uses the module tree to build its dependency injection container. When a controller asks for a service, Nest only looks inside that controller's own module and the modules it imports — this boundary is what keeps large applications organized.

The @Module() Decorator

PropertyPurpose
providersServices and other providers created by this module and available inside it.
controllersControllers defined in this module, which Nest instantiates and routes to.
importsOther modules whose exported providers this module needs.
exportsThe subset of this module's providers that importing modules are allowed to use.
Encapsulation by Default

Providers are private to their module unless they are listed in exports. This is deliberate — it stops modules from reaching into each other's internals.

Creating a Feature Module

nest g module users
nest g controller users
nest g service users
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller.js';
import { UsersService } from './users.service.js';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}

The CLI also adds UsersModule to the imports array of AppModule automatically. That one import is what connects the whole users feature to your application:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module.js';
@Module({
imports: [UsersModule],
})
export class AppModule {}

Sharing Providers with Exports

Suppose an OrdersService needs to look up users. Instead of creating a second UsersService, UsersModule exports its service, and OrdersModule imports UsersModule:

// src/users/users.module.ts
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // now other modules may use it
})
export class UsersModule {}
// src/orders/orders.module.ts
@Module({
imports: [UsersModule], // brings in UsersService
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}
// src/orders/orders.service.ts
@Injectable()
export class OrdersService {
constructor(private readonly usersService: UsersService) {}
}

Providers are singletons by default, so OrdersModule receives the exact same UsersService instance that UsersModule uses — modules share one instance rather than each creating their own.

Global Modules

Some providers — configuration, logging, a database connection — are needed almost everywhere. Importing their module into every feature module gets repetitive, so Nest lets you mark a module as global with @Global():

import { Global, Module } from '@nestjs/common';
import { LoggerService } from './logger.service.js';
@Global()
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}

Register a global module once (usually in AppModule) and its exported providers become injectable everywhere without further imports.

Use @Global() Sparingly

Making everything global defeats the purpose of modules — it hides which features depend on what. Reserve it for genuinely cross-cutting infrastructure.

Dynamic Modules

A dynamic module is configured at import time by calling a static method, conventionally named forRoot() (configure once for the whole app) or forFeature() (configure per feature module). You will use many of these from official packages:

@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }), // lesson 11
TypeOrmModule.forRoot({ /* db options */ }), // lesson 12
UsersModule,
],
})
export class AppModule {}

You rarely need to write your own dynamic module as a beginner, but recognizing the forRoot()/forFeature() pattern makes the official packages much easier to understand.

Common Mistakes

Avoid These Mistakes
  • Adding the same service to the providers array of two modules — this creates two separate instances instead of sharing one. Export it and import the module instead.
  • Importing a module but forgetting that the provider you need was never exported.
  • Listing a provider in exports without also listing it in providers (or importing it).
  • Putting every feature directly inside AppModule instead of creating feature modules.

Best Practices

  • Create one module per business feature (users, products, orders, auth).
  • Export only what other modules genuinely need — keep the rest private.
  • Keep AppModule thin: it should mostly import feature and infrastructure modules.
  • Use @Global() only for cross-cutting infrastructure like configuration or logging.

Frequently Asked Questions

It almost always means the provider X needs is not available in X's module — either it is not in providers, or its module is not imported, or it is not exported from the module that owns it.

Circular imports are possible using forwardRef(), but they usually signal a design problem. Prefer extracting the shared logic into a third module both can import.

Yes — a module can list an imported module in its exports, so anything importing it also gets that module's exported providers.

Key Takeaways

  • Modules are classes decorated with @Module() that group related functionality.
  • @Module() has four properties: providers, controllers, imports and exports.
  • Providers are private to their module unless exported.
  • Exported providers are shared as a single instance across importing modules.
  • @Global() modules and dynamic modules (forRoot/forFeature) handle app-wide infrastructure.

Summary

You learned how modules organize a Nest app into features, how imports and exports share providers safely, and when global and dynamic modules are useful. Next, you will build the entry points of your API — controllers and routing.

Next Lesson →

Controllers & Routing