2

I'm working on a NestJS application with TypeORM and PostgreSQL. My environment variables are not being loaded when the application starts, even though I can see dotenv injecting them in the console output. This results in a "password authentication failed" error because the database configuration receives undefined values. Here's what I see in the logs:

[[email protected]] injecting env (5) from .env – 🔐 encrypt with dotenvx: https://dotenvx.com
[Nest] 11840  - 06/29/2025, 4:18:13 PM     LOG [NestFactory] Starting Nest application...
[Nest] 11840  - 06/29/2025, 4:18:13 PM   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
error: password authentication failed for user "user"

When I debug the environment variables in my app.module.ts, they all show as undefined: Raw Environment Variables: POSTGRES_HOST: undefined POSTGRES_PORT: undefined POSTGRES_USER: undefined POSTGRES_PASSWORD: undefined POSTGRES_DATABASE: undefined All env keys containing POSTGRES: [] Current Configuration My .env file (located in project root): env

POSTGRES_HOST=localhost POSTGRES_PORT=5433 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_DATABASE=course-management

My app.module.ts: typescript

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CourseModule } from './course/course.module';
import { ScoreModule } from './score/score.module';

// Debug logging
console.log('Environment Variables:', {
  host: process.env.POSTGRES_HOST,
  port: process.env.POSTGRES_PORT,
  user: process.env.POSTGRES_USER,
  database: process.env.POSTGRES_DATABASE,
  passwordSet: !!process.env.POSTGRES_PASSWORD
});

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.POSTGRES_HOST || 'localhost',
      port: parseInt(process.env.POSTGRES_PORT || '5433'),
      password: String(process.env.POSTGRES_PASSWORD || ''),
      username: process.env.POSTGRES_USER || 'postgres',
      entities: [],
      database: process.env.POSTGRES_DATABASE || 'course-management',
      synchronize: true,
      logging: true,
    }),
    CourseModule,
    ScoreModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

My main.ts: typescript

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

dotenv.config();

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

What I Expected vs What Happened Expected: The environment variables should be loaded and available when app.module.ts is evaluated, allowing TypeORM to connect to PostgreSQL with the correct credentials. What Actually Happened:

dotenv shows it's injecting 5 variables from .env But when app.module.ts tries to read them, they're all undefined TypeORM tries to connect with default/undefined values Connection fails with "password authentication failed for user 'user'" (should be 'postgres')

What I've Tried

Verified .env file location: It's in the project root directory Checked .env file format: No spaces around equals signs Added explicit string conversion: String(process.env.POSTGRES_PASSWORD) Added debug logging: Confirms all variables are undefined Installed dotenv and types: npm install dotenv @types/dotenv Verified database credentials: Can connect manually with psql using the same credentials

The issue seems to be a timing problem where app.module.ts is evaluated before the environment variables are loaded, despite calling dotenv.config() in main.ts. Questions

Why are the environment variables undefined in app.module.ts even though dotenv claims to inject them? What's the correct way to ensure environment variables are available during module initialization in NestJS? Is there a better pattern than using dotenv directly with NestJS and TypeORM?

Environment:

NestJS 10.x TypeORM PostgreSQL dotenv 17.0.0 Node.js (Windows)

4 Answers 4

1

You can also use @nestjs/config method without using dotenv, ConfigModule will read from whatever is in process.env at runtime.

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

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
  ],
})
export class AppModule {}
Sign up to request clarification or add additional context in comments.

Comments

1

The problem is that your code has common pitfall which is incorrectly using dotenv with import:

import * as dotenv from 'dotenv';

dotenv.config();

should be:

import 'dotenv/config' 

see:

// Note: this is INCORRECT and will not work
import * as dotenv from 'dotenv'
dotenv.config()

import errorReporter from './errorReporter.mjs' // process.env.API_KEY will be blank!

How do I use dotenv with import?

and it shows why is using ConfigModule and ConfigService more appropriate way, because it makes it available for other dependencies on time.

Comments

0

I solved this by switching to @nestjs/config, which handles .env loading properly and integrates well with TypeORM using async configuration.

Previously, I used dotenv.config() in main.ts, but that ran too late. By the time it executed, AppModule had already been evaluated, and all my environment variables were still undefined. This caused TypeORM to attempt connecting with missing credentials.

Here’s the setup that worked for me

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

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get<string>('POSTGRES_HOST'),
        port: config.get<number>('POSTGRES_PORT', 5432),
        username: config.get<string>('POSTGRES_USER'),
        password: config.get<string>('POSTGRES_PASSWORD'),
        database: config.get<string>('POSTGRES_DATABASE'),
        synchronize: config.get<boolean>('TYPEORM_SYNCHRONIZE', false),
        autoLoadEntities: true,
        logging: config.get<boolean>('TYPEORM_LOGGING', false),
      }),
    }),
  ],
})
export class AppModule {}

This setup ensures that all environment variables are loaded at the framework level before any modules or decorators attempt to use them. As a result, TypeORM now initializes with the correct database credentials, and none of the environment variables are undefined at runtime.

Comments

0

If you're going to use dotenv's config method, it needs to be called before any other imports to ensure that the process.env object is properly loaded before accessing the variables in the decorators.

import * as dotenv from 'dotenv';
dotenv.config();

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

3 Comments

"it needs to be called before any other imports" - yes, but the code you show is not how to do it. (It may work, at best, if you transpile it to commonjs)
Ah, are you mentioning how some bundlers and ts runners will handle the imports first? I've not ran into that firsthand, as I generally have just used tsc to transpile to common and run it from there (as that's what Nest's CLI does)
I'm mentioning how the ECMAScript standard (and by extension, TypeScript) handles imports. I'd recommend switching to native modules if you can, but I don't know Nest.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.