The ConfigModule

Load settings once at startup and inject them through ConfigService instead of reading process.env all over the code.

SyntaxConfigModule.forRoot(options?: ConfigModuleOptions): Promise<DynamicModule>

Settings live outside the code: a port, a database URL, an API key. Reading process.env wherever you need them scatters string literals through the app and gives you no single place to see what the app requires. @nestjs/config reads the environment once at startup and hands the values out through a provider.

The three words

A module is a class marked with @Module that groups providers and declares which ones it imports from elsewhere and exports to others. A provider is any value the Nest injector constructs and passes into other constructors. A token is the key the injector files a provider under; for a class provider the token is the class itself.

How it works

ConfigModule.forRoot() is a dynamic module: a module configured by a method call rather than only by its decorator. The call parses the .env file, copies every key that is not already in process.env onto it, and registers ConfigService as a provider. You then inject ConfigService and call get() or getOrThrow().

All of that happens during bootstrap, before the server listens — long before middleware, guards, interceptors, pipes or any route handler run. By the time a request arrives, configuration is a plain object in memory.

What trips people up

In @nestjs/config 12, forRoot() is async and returns Promise<DynamicModule>. Nest's imports array accepts a promise, so normal code is unchanged, but you cannot read properties off the return value. And without isGlobal: true, only modules that import ConfigModule can inject ConfigService.

Example

Example · typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';

@Module({
  imports: [
    // forRoot() returns Promise<DynamicModule> in @nestjs/config 12.
    // ModuleMetadata['imports'] accepts a promise, so there is nothing to await.
    ConfigModule.forRoot({
      isGlobal: true, // ConfigService is injectable everywhere; no re-importing
      envFilePath: '.env', // the default: resolved against process.cwd()
      cache: true, // cache process.env reads in memory instead of re-reading
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

When to use it

  • Read the HTTP port and global route prefix in main.ts instead of hard-coding 3000.
  • Give a mail or payments service its credentials without importing process.env into the service file.
  • Fail fast on a missing API key with getOrThrow rather than sending a request with the header undefined.
  • Keep a service testable by swapping ConfigService for a stub in the testing module.

More examples

Injecting ConfigService, shorthand and longhand

Every Nest service you will read uses the parameter-property form, so the long form is worth recognising once: the injector only ever sees a constructor parameter whose type is the token. Reach for getOrThrow when the app cannot work without the value, and get with a default when there is a sensible fallback.

Example · typescript
// src/mail/mail.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class MailService {
  // Shorthand: a constructor parameter property declares the field,
  // marks it readonly, and assigns the injected value.
  constructor(private readonly config: ConfigService) {}

  endpoint(): string {
    // getOrThrow throws TypeError('Configuration key "SMTP_HOST" does not exist')
    // instead of returning undefined.
    const host = this.config.getOrThrow<string>('SMTP_HOST');
    // The second argument is a default, used only when the key is missing.
    const port = Number(this.config.get<string>('SMTP_PORT', '587'));
    return `${host}:${port}`;
  }
}

// Longhand: exactly what the constructor above desugars to.
// The injector treats these two classes identically.
@Injectable()
export class MailServiceLonghand {
  private readonly config: ConfigService;

  constructor(config: ConfigService) {
    this.config = config;
  }
}

Reading config outside dependency injection

main.ts has no constructor to inject into, so it asks the container directly. This is the right place to read the port, the global prefix or CORS origins. Avoid app.get() inside normal services: it hides the dependency from the injector and from anyone reading the constructor.

Example · typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module.js';

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

  // app.get() pulls a provider out of the container by its token.
  // ConfigModule.forRoot() has already run by this point, because
  // creating the app resolves every module in AppModule's import tree.
  const config = app.get(ConfigService);

  await app.listen(config.get<string>('PORT') ?? 3000);
}

void bootstrap();

Scoping ConfigService without isGlobal

Use this when you would rather see, module by module, which parts of the app depend on configuration. The cost is one import line per module; the benefit is that an accidental dependency on config shows up in the module file. forRoot() is still called exactly once, in the root module.

Example · typescript
// src/billing/billing.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { BillingService } from './billing.service.js';

@Module({
  // Needed only when forRoot() was called WITHOUT isGlobal: true.
  // Importing the bare ConfigModule class does not re-read .env and does not
  // re-run forRoot(); it only exposes the already-registered ConfigService
  // to this module's providers.
  imports: [ConfigModule],
  providers: [BillingService],
})
export class BillingModule {}

Discussion

  • Be the first to comment on this lesson.