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.
Recommended Solutions
1. Using ConfigModule with Async Configuration
The most robust approach is to use the asynchronous configuration pattern provided by NestJS database modules:
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:
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:
// 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:
// 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:
{
"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:
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:
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:
// 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:
{
"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.