Skip to content
NestJS ns config 4 min read

Configuration with ConfigModule

Hard-coding ports, database URLs, and API keys into your source is a recipe for leaked secrets and brittle deployments. NestJS solves this with the official @nestjs/config package, a thin, type-aware wrapper around dotenv that loads .env files at startup and exposes every value through an injectable ConfigService. Register it once in the root module and any provider in the application can read configuration safely, with full TypeScript support. This page walks through installing the package, registering ConfigModule.forRoot, and reading values with ConfigService.get.

Installing the package

@nestjs/config ships separately from the core framework. Install it from npm; it already bundles dotenv as a dependency, so there is nothing else to add.

npm install @nestjs/config

Create a .env file in your project root. The Nest CLI does not generate one for you, and you should keep it out of version control.

# .env
PORT=4000
DATABASE_URL=postgres://localhost:5432/app
JWT_SECRET=super-secret-dev-key
FEATURE_FLAGS=beta,reporting

Add .env to .gitignore and commit a .env.example with placeholder values instead. Never check real secrets into source control.

Registering ConfigModule

Configuration belongs in the root AppModule. Call ConfigModule.forRoot() in the imports array. By default it reads a .env file from the project root and merges it into process.env.

The single most important option is isGlobal: true. Without it, you would have to import ConfigModule into every feature module that needs configuration. Marking it global registers ConfigService once and makes it injectable everywhere.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      cache: true,
      envFilePath: ['.env.local', '.env'],
    }),
  ],
})
export class AppModule {}

The most commonly used forRoot options are summarized below.

OptionTypeEffect
isGlobalbooleanRegisters ConfigService globally so feature modules need not re-import the module
envFilePathstring | string[]One or more .env files; earlier entries win when keys overlap
cachebooleanCaches process.env lookups in memory for faster reads
ignoreEnvFilebooleanSkips file loading and relies only on the real environment (useful in production/containers)
expandVariablesbooleanEnables ${VAR} interpolation between variables inside .env

When envFilePath lists multiple files, the first file that defines a key wins. Putting .env.local ahead of .env lets each developer override shared defaults without touching the committed file.

Reading values with ConfigService

Inject ConfigService like any other provider and call get. Pass a type parameter so the returned value is correctly typed — values from process.env are always strings, so get<number>('PORT') documents the intended shape but does not coerce. Use the second argument to supply a default when a key is absent.

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

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

  const port = config.get<number>('PORT', 3000);
  await app.listen(port);

  console.log(`Listening on http://localhost:${port}`);
}
bootstrap();

Output:

Listening on http://localhost:4000

Inside a provider, inject ConfigService through the constructor. The getOrThrow variant is invaluable for required secrets: it throws immediately at resolution time if the key is missing, turning a silent undefined into a loud startup failure.

// src/auth/token.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class TokenService {
  private readonly secret: string;
  private readonly features: string[];

  constructor(private readonly config: ConfigService) {
    // Throws if JWT_SECRET is not defined — fail fast.
    this.secret = this.config.getOrThrow<string>('JWT_SECRET');
    // Parse a comma-separated list; default to an empty array.
    this.features = this.config.get<string>('FEATURE_FLAGS', '').split(',').filter(Boolean);
  }

  isEnabled(flag: string): boolean {
    return this.features.includes(flag);
  }
}

get vs getOrThrow

MethodMissing key behaviorUse when
get<T>(key)Returns undefinedThe value is optional
get<T>(key, default)Returns the defaultA sensible fallback exists
getOrThrow<T>(key)Throws at call timeThe value is mandatory (secrets, connection strings)

Configuration precedence

@nestjs/config does not blindly overwrite the runtime environment. Variables that already exist in process.env — for example those injected by a container orchestrator or CI system — take precedence over the same key in a .env file. This is exactly what you want in production: your .env file supplies local defaults while real deployments override them through genuine environment variables. Set ignoreEnvFile: true in production to lean entirely on the platform.

Best Practices

  • Always register ConfigModule.forRoot({ isGlobal: true }) once in AppModule so ConfigService is injectable everywhere without repeated imports.
  • Keep real secrets out of git: gitignore .env, commit a .env.example, and inject production values as real environment variables.
  • Use getOrThrow for mandatory keys so a missing secret fails loudly at startup rather than surfacing as a cryptic runtime bug.
  • Pass type parameters to get and remember values are strings — coerce or parse numbers, booleans, and lists explicitly.
  • Enable cache: true to avoid repeated process.env lookups in hot paths.
  • Layer .env files via envFilePath (e.g. .env.local before .env) so developers can override shared defaults without editing committed files.
Last updated June 14, 2026
Was this helpful?