Per-Environment Database Config

Read DB Settings From Config, Not Code

Per-Environment Database Config

Hard-coded connection strings are fine until they aren't. Move them into ConfigService and let the environment decide.

4 min read Level 2/5 #nestjs#typeorm#config
What you'll learn
  • Set up TypeOrmModule.forRootAsync
  • Inject ConfigService into the factory
  • Switch env files per NODE_ENV

Production runs on different credentials than dev. Tests use a different database entirely. The clean way to handle this in Nest is to load config from environment files and pass it to TypeORM (or Prisma, or Mongoose) through an async factory.

ConfigModule

@nestjs/config reads .env files into a typed ConfigService. Make it global so any module can inject it.

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

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

The envFilePath array is checked in order. So NODE_ENV=test reads .env.test, falling back to .env for anything missing.

forRootAsync With a Factory

forRootAsync accepts an imports array and an inject array, then calls the factory with whatever’s listed there. This is how you get the config in.

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

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (cfg: ConfigService) => ({
        type: 'postgres',
        host: cfg.get<string>('DB_HOST'),
        port: cfg.get<number>('DB_PORT'),
        username: cfg.get<string>('DB_USER'),
        password: cfg.get<string>('DB_PASS'),
        database: cfg.get<string>('DB_NAME'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: false,
      }),
    }),
  ],
})
export class AppModule {}

Now .env.development, .env.test, and .env.production can each point at their own database without a code change.

Validate the Config at Boot

It’s tempting to skip validation until things break. Don’t — a typo in an env var name should fail the build, not the first query.

import * as Joi from 'joi';

ConfigModule.forRoot({
  isGlobal: true,
  validationSchema: Joi.object({
    NODE_ENV: Joi.string().valid('development', 'test', 'production').required(),
    DB_HOST: Joi.string().required(),
    DB_PORT: Joi.number().default(5432),
    DB_USER: Joi.string().required(),
    DB_PASS: Joi.string().required(),
    DB_NAME: Joi.string().required(),
  }),
});

Boot fails loudly if DB_NAME is missing, and you’ll never spend an hour wondering why a deploy connects to the wrong host.

Passport.js in Nest →