Skip to content
NestJS ns fundamentals 4 min read

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 exports become 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

ConcernGood candidate for global?Why
Configuration (ConfigService)YesNeeded almost everywhere; stateless reads
LoggingYesCross-cutting, used by nearly all services
Database / connection providersSometimesConvenient, but explicit imports document data dependencies
Domain services (e.g. BillingService)NoBelongs to a bounded context; should be imported
Anything used by 1-2 modulesNoThe 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 dynamic forRoot() entry point).
  • Prefer isGlobal: true / global: true on 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 imports map.
Last updated June 14, 2026
Was this helpful?