Using Environment Variables in NestJS AppModule for Database Connections: A Complete Guide
When building a NestJS application with a database like PostgreSQL or MySQL, hardcoding connection details in your AppModule
is a recipe for trouble—security risks, inflexibility, and maintenance headaches. Instead, environment variables are the way to go. In this post, inspired by a Stack Overflow question and the NestJS Configuration Documentation, I’ll show you how to use environment variables in your AppModule
for TypeORM database connections, with a clean and secure setup.
Why Use Environment Variables?
Environment variables keep sensitive data like database credentials out of your codebase. They allow you to:
- Secure your app: Avoid exposing passwords in version control.
- Switch environments easily: Use different settings for development, staging, and production.
- Simplify configuration: Update settings without changing code.
The challenge, as seen in the Stack Overflow post, is accessing these variables in AppModule
, which is loaded early in a NestJS app. Let’s explore how to do this using the @nestjs/config
package.
The Problem: Accessing Environment Variables in AppModule
A common approach is to use process.env
directly in AppModule
for TypeORM configuration, like this:
1import { Module } from '@nestjs/common'; 2import { TypeOrmModule } from '@nestjs/typeorm'; 3 4@Module({ 5 imports: [ 6 TypeOrmModule.forRoot({ 7 type: 'postgres', 8 host: process.env.DB_HOST, 9 port: Number(process.env.DB_PORT), 10 username: process.env.DB_USERNAME, 11 password: process.env.DB_PASSWORD, 12 database: process.env.DB_NAME, 13 autoLoadEntities: true, 14 synchronize: false, 15 }), 16 ], 17}) 18export class AppModule {}
However, if process.env.DB_HOST
or other variables are undefined—perhaps because the .env
file wasn’t loaded or parsed correctly—you’ll hit errors like failed database connections. The @nestjs/config
package solves this by providing a robust configuration system.
Solution: Using @nestjs/config with TypeORM
The @nestjs/config
package, as outlined in the NestJS documentation, lets you load environment variables from .env
files, validate them, and inject them into your application. Here’s how to set it up.
Step 1: Install Dependencies
Install the required packages:
1npm install @nestjs/config @nestjs/typeorm typeorm pg
Step 2: Create a .env File
Create a .env
file in your project root with your database credentials:
1DB_HOST=localhost 2DB_PORT=5432 3DB_USERNAME=your_username 4DB_PASSWORD=your_password 5DB_NAME=your_database
Step 3: Configure @nestjs/config in AppModule
In src/app.module.ts
, use ConfigModule
to load the .env
file and ConfigService
to access variables. Pass these to TypeOrmModule.forRootAsync
for dynamic configuration.
1import { Module } from '@nestjs/common'; 2import { ConfigModule, ConfigService } from '@nestjs/config'; 3import { TypeOrmModule } from '@nestjs/typeorm'; 4 5@Module({ 6 imports: [ 7 ConfigModule.forRoot({ 8 isGlobal: true, // Makes ConfigModule available everywhere 9 envFilePath: '.env', // Path to .env file 10 }), 11 TypeOrmModule.forRootAsync({ 12 imports: [ConfigModule], 13 useFactory: (configService: ConfigService) => ({ 14 type: 'postgres', 15 host: configService.get<string>('DB_HOST'), 16 port: configService.get<number>('DB_PORT'), 17 username: configService.get<string>('DB_USERNAME'), 18 password: configService.get<string>('DB_PASSWORD'), 19 database: configService.get<string>('DB_NAME'), 20 autoLoadEntities: true, 21 synchronize: false, // Set to false in production 22 }), 23 inject: [ConfigService], 24 }), 25 ], 26}) 27export class AppModule {}
ConfigModule.forRoot({ isGlobal: true })
loads the.env
file and makesConfigService
available globally.TypeOrmModule.forRootAsync
usesuseFactory
to dynamically configure TypeORM with environment variables.inject: [ConfigService]
ensuresConfigService
is available in the factory.
Step 4: Add a Sample Entity and Module
Create a simple entity, e.g., src/users/user.entity.ts
:
1import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; 2 3@Entity() 4export class User { 5 @PrimaryGeneratedColumn() 6 id: number; 7 8 @Column() 9 name: string; 10}
Create a UsersModule
in src/users/users.module.ts
:
1import { Module } from '@nestjs/common'; 2import { TypeOrmModule } from '@nestjs/typeorm'; 3import { User } from './user.entity'; 4 5@Module({ 6 imports: [TypeOrmModule.forFeature([User])], 7}) 8export class UsersModule {}
Update AppModule
to import UsersModule
:
1import { Module } from '@nestjs/common'; 2import { ConfigModule, ConfigService } from '@nestjs/config'; 3import { TypeOrmModule } from '@nestjs/typeorm'; 4import { UsersModule } from './users/users.module'; 5 6@Module({ 7 imports: [ 8 ConfigModule.forRoot({ 9 isGlobal: true, 10 envFilePath: '.env', 11 }), 12 TypeOrmModule.forRootAsync({ 13 imports: [ConfigModule], 14 useFactory: (configService: ConfigService) => ({ 15 type: 'postgres', 16 host: configService.get<string>('DB_HOST'), 17 port: configService.get<number>('DB_PORT'), 18 username: configService.get<string>('DB_USERNAME'), 19 password: configService.get<string>('DB_PASSWORD'), 20 database: configService.get<string>('DB_NAME'), 21 autoLoadEntities: true, 22 synchronize: false, 23 }), 24 inject: [ConfigService], 25 }), 26 UsersModule, 27 ], 28}) 29export class AppModule {}
Step 5: Test the Setup
Run your PostgreSQL database locally (e.g., via Docker: docker run -e POSTGRES_USER=your_username -e POSTGRES_PASSWORD=your_password -e POSTGRES_DB=your_database -p 5432:5432 postgres
).
Start your NestJS app:
1npm run start:dev
If configured correctly, the app connects to the database using the .env
variables. You can verify by creating a controller to test the User
entity.
Step 6: Validate Environment Variables (Optional)
To ensure variables are present and valid, use @nestjs/config
’s validation feature with a library like joi
. Install it:
1npm install joi
Update AppModule
:
1import { Module } from '@nestjs/common'; 2import { ConfigModule, ConfigService } from '@nestjs/config'; 3import { TypeOrmModule } from '@nestjs/typeorm'; 4import * as Joi from 'joi'; 5import { UsersModule } from './users/users.module'; 6 7@Module({ 8 imports: [ 9 ConfigModule.forRoot({ 10 isGlobal: true, 11 envFilePath: '.env', 12 validationSchema: Joi.object({ 13 DB_HOST: Joi.string().required(), 14 DB_PORT: Joi.number().required(), 15 DB_USERNAME: Joi.string().required(), 16 DB_PASSWORD: Joi.string().required(), 17 DB_NAME: Joi.string().required(), 18 }), 19 }), 20 TypeOrmModule.forRootAsync({ 21 imports: [ConfigModule], 22 useFactory: (configService: ConfigService) => ({ 23 type: 'postgres', 24 host: configService.get<string>('DB_HOST'), 25 port: configService.get<number>('DB_PORT'), 26 username: configService.get<string>('DB_USERNAME'), 27 password: configService.get<string>('DB_PASSWORD'), 28 database: configService.get<string>('DB_NAME'), 29 autoLoadEntities: true, 30 synchronize: false, 31 }), 32 inject: [ConfigService], 33 }), 34 UsersModule, 35 ], 36}) 37export class AppModule {}
This ensures the app fails to start if required variables are missing or invalid, providing clear error messages.
Best Practices from NestJS Documentation
- Use .env Files for Each Environment: Create
.env.development
,.env.production
, etc., and specify them inConfigModule.forRoot({ envFilePath: ['.env.development', '.env'] })
to load the appropriate file. - Make ConfigModule Global: Set
isGlobal: true
to avoid importingConfigModule
in every module. - Validate Environment Variables: Use
joi
or similar to enforce required variables and types. - Avoid synchronize in Production: Set
synchronize: false
in TypeORM to prevent accidental schema changes. - Secure Your .env File: Add
.env
to.gitignore
to keep it out of version control.
Common Pitfalls
- Missing .env File: Ensure the
.env
file exists and is correctly referenced inConfigModule
. - Incorrect Variable Types: Use
configService.get<number>('DB_PORT')
to cast types explicitly. - Early Loading Issues: Since
AppModule
loads early, ensureConfigModule
is initialized first withforRoot
. - Environment Overrides: If variables are set in the system environment, they override
.env
file values.
Conclusion
Using environment variables in your NestJS AppModule
for database connections is straightforward with @nestjs/config
. By setting up a .env
file, configuring ConfigModule
, and using TypeOrmModule.forRootAsync
, you can create a secure, flexible, and maintainable setup. Validation with joi
adds an extra layer of robustness.
Have you tackled environment variables in your NestJS projects? Share your tips or questions in the comments below!
Happy coding, and keep building secure NestJS apps!