Back to Blog
NestJS@nestjs/config

NestJS Practical Guide: How We Manage Configuration and Support Multiple Environments

RaytonX
3 min read

In medium to large-scale NestJS projects, configuration management is a key factor for system stability.
How can we elegantly load .env files? How do we implement type-safe validation? How do we distinguish configurations between development, testing, and production environments?
This article will share how we build a clear, reliable, and scalable configuration system in our projects.

1. Why is configuration management important?

  • Disorganized .env files and hard-coded environment variables
  • Unclear environment switching causing frequent deployment errors
  • Lack of validation leading to runtime errors caused by missing configurations
  • Multiple modules depending on configurations, resulting in repeated imports and maintenance difficulties

We built a composable, validated, and easily extendable configuration solution using the @nestjs/config module combined with a configuration loader and schema validation.

2. Configuring ConfigService

1. Install dependencies

pnpm add @nestjs/config joi

2. Project structure

src/
├── config/
│   ├── configuration.ts         
│   ├── validation.ts            
│   └── config.interface.ts   
├── app.module.ts
├── main.ts
├── .env
├── .env.development
├── .env.production
├── .env.test

3. Configuration loader (configuration.ts)

Centralize all configuration items from .env files for better type completion and reuse across modules.

// src/config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT ?? '3000', 10),
  database: {
    uri: process.env.MONGODB_URI,
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN ?? '1d',
  },
});

4. Configuration validation (validation.ts)

Use Joi to strongly validate configuration values before application startup, preventing runtime issues due to invalid or missing configurations.

// src/config/validation.ts
import * as Joi from 'joi';

export const validationSchema = Joi.object({
  PORT: Joi.number().default(3000),
  MONGODB_URI: Joi.string().uri().required(),
  JWT_SECRET: Joi.string().required(),
  JWT_EXPIRES_IN: Joi.string().default('1d'),
});

5. Import configuration module in AppModule

Set the configuration module as global and support loading multiple .env files based on the environment.

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

const getEnvFilePath = (): string[] => {
  const env = process.env.NODE_ENV;
  const base = '.env';
  const mapping: Record<string, string> = {
    production: '.env.production',
    development: '.env.development',
    test: '.env.test',
  };
  return [mapping[env ?? 'development'], base];
};

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [configuration],
      validationSchema,
      envFilePath: getEnvFilePath(),
    }),
    // ...other modules
  ],
})
export class AppModule {}

3. Using ConfigService

Inject ConfigService in any service or module to retrieve configuration values with type safety.

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AuthService {
  constructor(private readonly configService: ConfigService) {}

  getJwtSecret(): string {
    return this.configService.get<string>('jwt.secret');
  }
}

Start your project with the correct environment variable, for example:

NODE_ENV=production node dist/main.js

4. Injecting Environment Variables with Kubernetes

In the production environment, environment variables are injected via Kubernetes, and the .env.production file must not be included in the Docker image.

1. Update getEnvFilePath

const getEnvFilePath = (): string[] => {
  const env = process.env.NODE_ENV;
  const base = '.env';
  const mapping: Record<string, string> = {
    development: '.env.development',
    test: '.env.test',
  };
  return env === 'production' ? undefined : [mapping[env ?? 'development'], base];
};

2. K8s Configuration

  1. configmap

    # configmap.yaml
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: myapp-config
    data:
      PORT: "3000"
      DATABASE_URL: "postgres://user:pass@host:5432/db"
    
  2. deployment

    # deployment.yaml
    spec:
      containers:
        - name: myapp
          image: your-image:tag
          envFrom:
            - configMapRef:
                name: myapp-config
    

If you are also using NestJS to build backend systems, consider adopting this approach to unify your configuration management logic and improve code robustness and maintainability.