Global Modules
In NestJS, providers are normally scoped to the module that declares and exports them. Any other module that wants those providers must explicitly import the owning module. The @Global() decorator breaks that rule: it promotes a module’s exported providers into the global scope so they become available everywhere after a single registration. This is convenient for truly cross-cutting concerns like configuration and logging, but it trades a little boilerplate for a lot of hidden coupling, so it should be used sparingly.
How module scoping works by default
Nest’s dependency injection is module-encapsulated. A provider declared in FeatureModule is only injectable inside FeatureModule unless the module exports it and the consuming module imports it. This explicit wiring is a feature, not a limitation: it makes the dependency graph readable and keeps modules loosely coupled.
// logging.module.ts
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggingModule {}
To use LoggerService in a UsersModule, you must import LoggingModule:
// users.module.ts
import { Module } from '@nestjs/common';
import { LoggingModule } from '../logging/logging.module';
import { UsersService } from './users.service';
@Module({
imports: [LoggingModule],
providers: [UsersService],
})
export class UsersModule {}
Registering a module as global
The @Global() decorator, placed above @Module(), registers the module once (typically in the root module) and makes its exported providers injectable across the entire application without further imports.
// logging.module.ts
import { Global, Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Global()
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggingModule {}
Now any service can inject LoggerService directly, provided LoggingModule is imported exactly once:
// app.module.ts
import { Module } from '@nestjs/common';
import { LoggingModule } from './logging/logging.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [LoggingModule, UsersModule],
})
export class AppModule {}
// users.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from '../logging/logger.service';
@Injectable()
export class UsersService {
constructor(private readonly logger: LoggerService) {}
findAll() {
this.logger.log('Fetching all users');
return [{ id: 1, name: 'Ada' }];
}
}
Output:
[Nest] 14820 - 06/14/2026, 10:12:04 AM LOG [LoggerService] Fetching all users
Notice that UsersModule no longer needs to import LoggingModule — the provider is resolved from the global registry.
Tip: Only the module’s
exportsbecome global. Providers that are declared but not exported remain private to the module even when@Global()is applied.
Globals and dynamic modules
The most common real-world use is a configuration module exposed globally so every feature can read settings. The @nestjs/config package does exactly this via isGlobal: true, which internally marks its dynamic module as global.
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
],
})
export class AppModule {}
When you build your own dynamic module, set global: true on the returned DynamicModule:
// config.module.ts
import { DynamicModule, Module } from '@nestjs/common';
@Module({})
export class AppConfigModule {
static forRoot(options: { folder: string }): DynamicModule {
const provider = {
provide: 'CONFIG_OPTIONS',
useValue: options,
};
return {
module: AppConfigModule,
global: true,
providers: [provider],
exports: [provider],
};
}
}
When global is appropriate — and when it is not
| Concern | Good candidate for global? | Why |
|---|---|---|
Configuration (ConfigService) | Yes | Needed almost everywhere; stateless reads |
| Logging | Yes | Cross-cutting, used by nearly all services |
| Database / connection providers | Sometimes | Convenient, but explicit imports document data dependencies |
Domain services (e.g. BillingService) | No | Belongs to a bounded context; should be imported |
| Anything used by 1-2 modules | No | The import is cheap and self-documenting |
The downside of global modules is invisibility. With explicit imports, the imports array of a module is an accurate map of its dependencies. Global providers bypass that map: a service can suddenly depend on something with no trace in any module declaration. This complicates refactoring, makes unit-test setup less obvious, and can mask circular or accidental dependencies.
Warning: Do not register the same global module in more than one place. Importing it once is enough; importing it again elsewhere creates a second provider instance and defeats the singleton guarantee.
Best Practices
- Reserve
@Global()for genuinely universal, cross-cutting providers — configuration and logging are the canonical examples. - Register each global module exactly once, in the root
AppModule(or its dynamicforRoot()entry point). - Prefer
isGlobal: true/global: trueon dynamic modules rather than the@Global()decorator when the module is configured at registration time. - Keep domain and feature services out of the global scope; import their owning modules explicitly so dependencies stay visible.
- Export only what consumers need — unexported providers stay private even in a global module.
- Treat global registration as a deliberate architectural decision and document it, since it removes a dependency from every module’s
importsmap.