Skip to content

Using Environment Variables in NestJS Database Configuration

Problem Statement

When developing NestJS applications, you often need to configure database connections using environment-specific variables rather than hardcoded values. The challenge arises when trying to access these environment variables in your main application module (app.module.ts) for database configuration.

The initial approach of using ConfigModule.get() directly within the module imports doesn't work because the configuration hasn't been loaded yet when the module is being instantiated.

1. Using ConfigModule with Async Configuration

The most robust approach is to use the asynchronous configuration pattern provided by NestJS database modules:

typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: `.env.${process.env.NODE_ENV}`,
    }),
    MongooseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        uri: configService.get<string>('MONGODB_URI'),
        dbName: configService.get<string>('DB_NAME'),
        useNewUrlParser: true,
      }),
      inject: [ConfigService],
    }),
    ProductModule,
    CategoryModule,
  ],
  controllers: [AppController, HealthCheckController],
  providers: [AppService, CustomLogger],
})
export class AppModule {}

2. Environment-Specific Configuration with ConfigModule

For more complex scenarios with multiple environment files:

typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      cache: true,
      envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'],
    }),
    // Other modules...
  ],
})
export class AppModule {}

3. Dedicated Database Module

Create a separate database module for better organization:

typescript
// database.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        uri: configService.get<string>('MONGODB_URI'),
        dbName: configService.get<string>('DB_NAME'),
        useNewUrlParser: true,
      }),
      inject: [ConfigService],
    }),
  ],
  exports: [MongooseModule],
})
export class DatabaseModule {}

Then import it in your main module:

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

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: `.env.${process.env.NODE_ENV}`,
    }),
    DatabaseModule,
    ProductModule,
    CategoryModule,
  ],
  // controllers and providers...
})
export class AppModule {}

Environment Setup

Package.json Scripts

Configure your package.json scripts to handle different environments:

json
{
  "scripts": {
    "start:local": "cross-env NODE_ENV=local npm run start",
    "start:dev": "cross-env NODE_ENV=development npm run start",
    "start:prod": "cross-env NODE_ENV=production npm run start"
  }
}

TIP

Install cross-env for cross-platform environment variable support:

bash
npm install -D cross-env

Environment Files

Create environment-specific files in your project root:

  • .env.local - Local development
  • .env.development - Development environment
  • .env.production - Production environment

Example .env.local file:

DB_USER=myusername
DB_PASS=mypassword
DB_HOST=myhost.net
DB_NAME=dbname
MONGODB_URI=mongodb+srv://myusername:mypassword@myhost.net/dbname?retryWrites=true&w=majority

Best Practices

1. Use Async Configuration

Always use asynchronous configuration (forRootAsync) when dealing with environment variables to ensure the ConfigService is properly initialized.

2. Make ConfigModule Global

Set isGlobal: true in your ConfigModule configuration to make it available throughout your application without needing to import it in every module.

3. Validate Environment Variables

Consider adding validation for your environment variables:

typescript
import * as Joi from 'joi';

ConfigModule.forRoot({
  validationSchema: Joi.object({
    NODE_ENV: Joi.string()
      .valid('development', 'production', 'test')
      .default('development'),
    MONGODB_URI: Joi.string().required(),
    DB_NAME: Joi.string().required(),
  }),
});

4. Use Full URIs for Database Connections

Instead of constructing connection strings manually, use the full URI format in your environment variables:

MONGODB_URI=mongodb+srv://username:password@host.net/dbname?retryWrites=true&w=majority

Common Pitfalls

WARNING

Avoid using ConfigModule.get() directly in module imports. The synchronous nature of module imports means the configuration may not be loaded yet.

DANGER

Never commit actual credentials in your environment files. Use .env.example files with placeholder values and add actual .env files to your .gitignore.

Alternative Approaches

Using Dotenv Directly

For simple cases, you can use the dotenv package directly:

typescript
// main.ts
import * as dotenv from 'dotenv';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

dotenv.config({ path: `.env.${process.env.NODE_ENV}` });

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

Using env-cmd

Alternatively, use the env-cmd package to load environment files:

json
{
  "scripts": {
    "start:dev": "env-cmd -f .env.development npm run start"
  }
}

Conclusion

The most effective approach for using environment variables in your NestJS database configuration is to combine the ConfigModule with asynchronous database module configuration. This ensures proper loading order, maintains clean code organization, and follows NestJS best practices.

By implementing these patterns, you can securely manage database credentials across different environments while maintaining a clean and maintainable codebase.