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.
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.