NestJS course · Module 1: NestJS Basics
Configuration Management - The Province Register
In this lesson8
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.
Four steps
Configuration enters a project in a fixed order - worth knowing as a whole, because skipping a step produces misleading errors:
- Create the
.envfile with keys and values. - Register
ConfigModule.forRoot()in the root module. - Inject
ConfigServicethrough the constructor. - Read the values with
configService.get('KEY').
Step one: the .env file
Environment variables live in a .env file in the project directory:
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 .env, not 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.
Step two: registering the module
Configuration support comes from the @nestjs/config package. Mind the name - there are no @nestjs/env, @nestjs/settings or @nestjs/environment packages. You install it with:
1npm i @nestjs/configWe 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 notation has a fixed frame: ConfigModule.forRoot({, then the options - here envFilePath: '.env', and isGlobal: true - and finally }). The order of the options inside the object does not matter; what matters is that a comma separates them.
envFilePath points at the file to load; with the default name it can be omitted.
isGlobal: true makes ConfigService available in every module with no additional imports. Without that option you would have to import 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.
Steps three and four: reading
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.
Checking at startup
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 separate schema description library, not part of NestJS: you install it with npm i joi (NestJS 12 requires Joi 18 or later) and import it with import * as Joi from 'joi';. @nestjs/config accepts such a schema in the validationSchema option.
Notice that each key has either .required() or .default(...). Combining both makes no sense: when a required key is missing, validation stops the startup before the default value could kick in.
Where a value comes from
The same key can live in several places at once. When it is both in the system environment variables (e.g. set on the server) and in the .env file, the system variable wins - the file will not override it. NestJS itself loads only .env; other files go in envFilePath, e.g. envFilePath: ['.env.production', '.env'], and then for a repeated key the first file on the list wins. The default value from configService.get('KEY', defaultValue) applies only when the key is nowhere at all.
Configuration groups: registerAs
As keys multiply, it is handy to group them by topic. The registerAs function from @nestjs/config creates a named group:
1import { registerAs } from '@nestjs/config';
2
3export const databaseConfig = registerAs('database', () => ({
4 host: process.env.DB_HOST,
5 port: parseInt(process.env.DB_PORT ?? '5432', 10),
6 name: process.env.DB_NAME,
7}));The first argument is the group name, the second a factory that returns an object with the values. Note the parseInt: environment variables are text, so you convert numbers yourself. You load the group with the load option - ConfigModule.forRoot({ load: [databaseConfig] }) - and read it with a dot: this.configService.get('database.host').
Summary
The province's register is read on the spot, the code is one for all of them:
- configuration lives outside the code, because the same application runs in several environments,
- four steps in order: the
.envfile →ConfigModule.forRoot()in the root module → injectingConfigService→configService.get('KEY'), - variables live in a
.envfile - notconfig.json, notsettings.yaml, notenvironment.xml, - format: name, equals sign, value; the file stays out of the repository, you commit
.env.example, - the package is
@nestjs/config;@nestjs/env,@nestjs/settingsand@nestjs/environmentdo not exist, - registration:
ConfigModule.forRoot({, options separated by commas (e.g.envFilePath: '.env',andisGlobal: true), finally}), isGlobal: truemakesConfigServiceavailable in all modules with no extra imports - it encrypts nothing and restricts nothing,- reading:
this.configService+.get+('DB_HOST'); values from.envalways arrive as text, get's second argument is a default value - do not give one to passwords or secrets,validationSchemawithJoichecks the full set of keys at startup and aborts the launch when one is missing; a key has.required()or.default(...), not both,- a system variable wins over the
.envfile, and with several files inenvFilePaththe first one wins, registerAs('database', () => ({ ... }))groups keys; you load the group withloadand read it withget('database.host').
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.
Code for this lesson: src/config-management.ts
1// Configuration Management - Managing Imperium Resources
2import { Module, Injectable } from '@nestjs/common';
3
4console.log("Configuration Management - quaestor of the imperium!");
5
6// ===========================================
7// 1. ConfigModule - basic configuration
8// ===========================================
9
10// app.module.ts
11// import { ConfigModule } from '@nestjs/config';
12//
13// @Module({
14// imports: [
15// ConfigModule.forRoot({
16// isGlobal: true, // Available across the entire imperium
17// envFilePath: '.env', // Path to config file
18// }),
19// ],
20// })
21// export class AppModule {}
22
23// ===========================================
24// 2. Using ConfigService
25// ===========================================
26
27// import { ConfigService } from '@nestjs/config';
28
29@Injectable()
30export class LegionConfigService {
31 // In a real app: constructor(private configService: ConfigService)
32
33 getPort(): number {
34 // return this.configService.get<number>('PORT', 3000);
35 return 4000; // Default imperium port
36 }
37
38 getDatabaseUrl(): string {
39 // return this.configService.get<string>('DATABASE_URL');
40 return 'mongodb://localhost:27017/imperium';
41 }
42
43 getJwtSecret(): string {
44 // return this.configService.get<string>('JWT_SECRET');
45 return 'roma-aeterna-secret';
46 }
47
48 isProduction(): boolean {
49 // return this.configService.get('NODE_ENV') === 'production';
50 return false;
51 }
52}
53
54// ===========================================
55// 3. Custom configuration files
56// ===========================================
57
58// config/legion.config.ts
59export const legionConfig = () => ({
60 legion: {
61 maxSoldiers: 6000,
62 minExperience: 1,
63 ranks: ['Miles', 'Optio', 'Centurio', 'Tribunus', 'Legatus'],
64 defaultProvince: 'Roma',
65 },
66 database: {
67 host: process.env.DB_HOST || 'localhost',
68 port: parseInt(process.env.DB_PORT || '5432', 10),
69 name: process.env.DB_NAME || 'imperium_db',
70 },
71 security: {
72 jwtExpiration: '7d',
73 bcryptRounds: 12,
74 maxLoginAttempts: 5,
75 },
76});
77
78// ===========================================
79// 4. Configuration validation with Joi
80// ===========================================
81
82// import * as Joi from 'joi';
83//
84// ConfigModule.forRoot({
85// validationSchema: Joi.object({
86// NODE_ENV: Joi.string()
87// .valid('development', 'production', 'test')
88// .default('development'),
89// PORT: Joi.number().default(4000),
90// DATABASE_URL: Joi.string().required(),
91// JWT_SECRET: Joi.string().required(),
92// }),
93// validationOptions: {
94// abortEarly: true,
95// },
96// })
97
98console.log("\n=== PODSUMOWANIE CONFIGURATION ===");
99console.log("ConfigModule.forRoot() - loading configuration");
100console.log("ConfigService.get() - retrieving values");
101console.log("load: [config] - custom configuration files");
102console.log("validationSchema - environment variable validation");
103Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which NestJS package is used for managing configuration and environment variables?
2. In which file do we store environment variables in a NestJS project?
3. What does setting isGlobal: true in ConfigModule.forRoot() mean?
4. Which package is most commonly used for validating environment variables in NestJS?
5. What is the main function of the main.ts file in NestJS?
6. What is the file naming convention in NestJS?
7. What is the correct order of HTTP request processing in NestJS?
Hands-on tasks in the game
- Code editor
Register ConfigModule.forRoot() in AppModule and use ConfigService.get('DB_HOST') in a service
- Vertical ordering
Arrange the steps for configuring environment variables from first to last:
- Click in order
Arrange the elements of ConfigModule registration in the correct order:
- Horizontal ordering
Arrange the elements for reading an environment variable via ConfigService:
- Code editor
Write a registerAs factory that groups DB_HOST, DB_PORT, and DB_NAME variables into a 'database' object
- Vertical ordering
Arrange NestJS configuration sources from highest to lowest priority:
- Horizontal ordering
Arrange the elements of the Joi schema validation for the PORT variable:
- Click in order
Arrange the elements of an environment variable definition in the .env file:
- Vertical ordering
Arrange the stages of starting a NestJS application from beginning to end:
- Code editor
Configure ConfigModule.forRoot() with validationSchema using Joi to validate PORT and DATABASE_URL
- Horizontal ordering
Arrange the elements of the ConfigModule import in the correct order:
- Vertical ordering
Arrange the files of a NestJS module from the most important (defining) to implementation:
- Horizontal ordering
Arrange the elements for creating a NestJS application instance:
- Click in order
Arrange the elements of the HTTP request lifecycle in NestJS in the correct order:
- Code editor
Create a bootstrap function that creates an app with NestFactory.create(), sets up a global ValidationPipe, and listens on port 3000