A database address written into the code works perfectly - on your laptop. On the staging server the database sits elsewhere, on production elsewhere again, and its password should never reach the repository at all. Three environments, one codebase, three different values - and none of them can be hard-coded.
Rome had a custom for this. Orders were written once, but a province's resources were recorded in the local register: how much grain, which road, who governs. The same legion marched into Gaul and into Egypt, reading the local register each time. In an application that register is the environment variables.
Configuration enters a project in a fixed order - worth knowing as a whole, because skipping a step produces misleading errors:
.env file with keys and values.ConfigModule.forRoot() in the root module.ConfigService through the constructor.configService.get('KEY').Environment variables live in a
file in the project directory:.env
1DB_HOST=localhost
2DB_PORT=5432
3DB_PASSWORD=tribute123
4JWT_SECRET=super-secret-keyThe format is minimal: name, equals sign, value. No quotes, no spaces around the equals sign, no semicolons at the end.
The file's name is not a convention you choose - it is
, not .env
config.json, settings.yaml or environment.xml. You will meet those formats in other ecosystems, but not here.The file itself never goes into the repository - you add it to
.gitignore. Instead you commit a .env.example with the same keys and empty or sample values, so a colleague knows what to fill in.Configuration support comes from the
package. Mind the name - there are no @nestjs/config
@nestjs/env, @nestjs/settings or @nestjs/environment packages.We register it in the root module:
1@Module({
2 imports: [
3 ConfigModule.forRoot({
4 envFilePath: '.env',
5 isGlobal: true,
6 }),
7 ],
8})
9export class AppModule {}The order of the elements is fixed:
, then the options - ConfigModule.forRoot({
and envFilePath: '.env',
- and finally isGlobal: true
.})
envFilePath points at the file to load; with the default name it can be omitted.
makes isGlobal: true
available in every module with no additional imports. Without that option you would have to import ConfigService
ConfigModule in each module separately - and usually half the application uses it. Note what this option does not do: it encrypts nothing, does not limit visibility to the root module, and does not refresh configuration on restart.You inject
ConfigService like any other dependency:1@Injectable()
2export class DatabaseService {
3 constructor(private configService: ConfigService) {}
4
5 getConnection() {
6 const host = this.configService.get('DB_HOST');
7 const port = this.configService.get<number>('DB_PORT', 5432);
8
9 return { host, port };
10 }
11}A read is three parts in a fixed order:
, this.configService
, .get
.('DB_HOST')
Two details are worth knowing. The
<number> notation is a type parameter - it tells TypeScript what you expect, because values from a .env file always arrive as text. The second argument, 5432 here, is a default value used when the key is missing. That is convenient, but be careful: a default password or a default JWT secret is a ready-made hole - for such values it is better that the application fails to start than that it starts with anything at hand, @name.Since a missing key yields
undefined, a failure surfaces only at first use - sometimes hours later. Better to check the full set at once:1ConfigModule.forRoot({
2 isGlobal: true,
3 validationSchema: Joi.object({
4 DB_HOST: Joi.string().required(),
5 DB_PORT: Joi.number().default(5432),
6 JWT_SECRET: Joi.string().required(),
7 }),
8});validationSchema describes which keys you expect and of what type. At startup ConfigModule compares it with the contents of .env and aborts the launch if anything is missing - with a message naming the key outright.That trades a three-in-the-morning outage for an error at deployment time.
Joi is a schema description library; it is not part of NestJS, but it is what @nestjs/config uses.The province's register is read on the spot, the code is one for all of them:
.env file → ConfigModule.forRoot() in the root module → injecting ConfigService → configService.get('KEY'),.env file - not config.json, not settings.yaml, not environment.xml,.env.example,@nestjs/config; @nestjs/env, @nestjs/settings and @nestjs/environment do not exist,ConfigModule.forRoot({, envFilePath: '.env',, isGlobal: true, }),isGlobal: true makes ConfigService available in all modules with no extra imports - it encrypts nothing and restricts nothing,this.configService + .get + ('DB_HOST'); values from .env always arrive as text,get's second argument is a default value - do not give one to passwords or secrets,validationSchema with Joi checks the full set of keys at startup and aborts the launch when one is missing.This is the module's last lesson. You can now build a module, a controller and a service, expose a REST API, describe data with DTOs and move configuration out of the code - everything an application needs to go out into the world. For now remember: the code is one for every province; only the register they read on arrival differs.