NestJS — Enterprise Node.js

Modules in Depth

15 min Lesson 6 of 48

Modules in Depth

Modules are how NestJS organises an application into cohesive, self-contained features. A small app might have one module, but real applications are a tree of feature modules — each owning its controllers, services, and dependencies. Mastering modules is what keeps a large codebase navigable.

Feature modules

A feature module groups everything related to one domain. Instead of cramming users, orders, and payments into the root module, each gets its own:

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

Then you wire it into the root module via imports:

// app.module.ts @Module({ imports: [UsersModule, OrdersModule, PaymentsModule], }) export class AppModule {}

Encapsulation: the key rule

Providers are private to their module by default. If UsersModule declares UsersService in providers but does not export it, no other module can inject it — even if it imports UsersModule.

This encapsulation is intentional. It keeps internal services hidden and forces you to be deliberate about what a module exposes — the same discipline as public vs private in a class.

Shared modules: exporting providers

To let other modules use a provider, add it to the module's exports array:

// users/users.module.ts @Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService], // now importable by other modules }) export class UsersModule {} // orders/orders.module.ts @Module({ imports: [UsersModule], // gains access to UsersService providers: [OrdersService], }) export class OrdersModule {}

Now OrdersService can inject UsersService because OrdersModule imports the module that exports it.

Re-exporting modules

A module can import and re-export another module, bundling related capabilities:

@Module({ imports: [DatabaseModule], exports: [DatabaseModule], // anything that imports this also gets DatabaseModule }) export class CoreModule {}

Global modules

Some providers are needed almost everywhere — a config service, a logger, a database connection. Marking a module @Global() registers its exports application-wide, so you import it once (in the root module) and inject its providers anywhere without repeating imports:

import { Module, Global } from '@nestjs/common'; @Global() @Module({ providers: [ConfigService], exports: [ConfigService], }) export class ConfigModule {}
Use @Global() sparingly. Making everything global defeats the purpose of modules — it hides dependencies and makes the architecture harder to reason about. Reserve it for truly cross-cutting concerns like config and logging.
Rule of thumb: a module should export only what other modules legitimately need. If a service is an internal implementation detail, keep it unexported.

Summary

Modules organise an app into encapsulated feature units. Providers are private unless exported; other modules gain access by importing the module that exports them. @Global() registers a module's exports everywhere — powerful but to be used sparingly. Next we go under the hood of how NestJS actually resolves these dependencies.