LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
Lesson 1116 min read

Configuration & Environment Variables

Manage settings across environments in NestJS with @nestjs/config: .env files, ConfigService, validation of required variables and namespaced configuration.

On this page 12 sections

Introduction

Your app behaves differently in different places: on your laptop it talks to a local database, in production to a managed one; locally the JWT secret can be anything, in production it must be strong and private. Configuration lets the same code run everywhere by reading these values from the environment instead of hard-coding them.

What You Will Learn
  • Why settings belong in environment variables.
  • How to load a .env file with ConfigModule.
  • How to read values safely with ConfigService.
  • How to fail fast when required variables are missing.
  • How to configure other modules (like JWT) from ConfigService.

Why Environment Variables?

  • Security — secrets like passwords and API keys never get committed to Git.
  • Portability — the same build runs in development, testing and production.
  • Flexibility — hosting platforms (Render, Railway, AWS, Docker) all inject settings as environment variables.

Installing @nestjs/config

npm install @nestjs/config

Create a .env file in the project root:

# .env
PORT=3000
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USER=root
DATABASE_PASSWORD=secret
DATABASE_NAME=shop
JWT_SECRET=change-me-to-a-long-random-string

Then load it once, globally, in AppModule:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
// ...feature modules
],
})
export class AppModule {}
Never Commit .env

Add .env to your .gitignore immediately. Commit a .env.example file instead, listing every variable name with placeholder values, so teammates know what to set.

Reading Values with ConfigService

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class MailService {
constructor(private readonly config: ConfigService) {}
getSender() {
// with a default value if the variable is not set
return this.config.get<string>('MAIL_FROM', 'no-reply@example.com');
}
getPort() {
// getOrThrow fails loudly if the variable is missing
return this.config.getOrThrow<number>('PORT');
}
}

You can use ConfigService in main.ts too, by fetching it from the app instance:

const app = await NestFactory.create(AppModule);
const config = app.get(ConfigService);
await app.listen(config.get<number>('PORT', 3000));
Everything Is a String

Environment variables are always strings. The generic in get<number>() only changes the TypeScript type — it does not convert "3306" into 3306. Convert explicitly, or use a validation schema (below) that coerces types.

Validating Configuration

If DATABASE_HOST is missing, it is far better for the app to refuse to start than to crash later on the first request. ConfigModule supports a Joi validation schema:

npm install joi
import Joi from 'joi';
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().port().default(3000),
DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.number().default(3306),
JWT_SECRET: Joi.string().min(32).required(),
}),
});
Startup with JWT_SECRET missing

Click Run to see what this code prints.

Namespaced Configuration

As settings grow, group them into typed objects with registerAs():

// src/config/database.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT ?? '3306', 10),
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
name: process.env.DATABASE_NAME,
}));
// src/app.module.ts
ConfigModule.forRoot({ isGlobal: true, load: [databaseConfig] });
// anywhere
const dbHost = this.config.get<string>('database.host');

Async Module Configuration

Modules like JwtModule and TypeOrmModule need configuration values at import time. Their registerAsync()/forRootAsync() variants let you build the options from ConfigService:

JwtModule.registerAsync({
global: true,
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.getOrThrow<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),

This replaces the hard-coded process.env.JWT_SECRET from lesson 9 and guarantees the .env file has been loaded before the value is read. You will use the same pattern for the database in the next lesson.

Common Mistakes

Avoid These Mistakes
  • Committing .env with real secrets to Git.
  • Reading process.env directly inside module decorators before ConfigModule has loaded the file.
  • Assuming get<number>() converts strings to numbers.
  • Forgetting isGlobal: true and then getting "can't resolve ConfigService" errors in feature modules.

Best Practices

  • Load ConfigModule once in AppModule with isGlobal: true.
  • Validate every required variable at startup.
  • Group related settings with registerAs() namespaces.
  • Use forRootAsync()/registerAsync() with ConfigService for other modules.
  • Keep a .env.example file up to date.

Frequently Asked Questions

Yes — pass envFilePath: [.env.${process.env.NODE_ENV}, '.env'] to forRoot(). The first file that defines a variable wins.

Yes. By default, variables already set in the process environment (for example by your hosting platform) take precedence over values in the .env file.

Key Takeaways

  • @nestjs/config loads .env files and exposes values through ConfigService.
  • ConfigModule.forRoot({ isGlobal: true }) makes ConfigService available everywhere.
  • A Joi validationSchema makes the app fail fast on missing or invalid settings.
  • registerAs() groups settings into namespaces like database.host.
  • Async module variants (registerAsync/forRootAsync) build options from ConfigService.

Summary

Your app now reads its settings safely from the environment and refuses to start without the essentials. Next, you will replace the in-memory arrays used so far with a real database, using TypeORM.

Next Lesson →

Databases with TypeORM