Suggest an editImprove this articleRefine the answer for “How to manage configuration in NestJS with configmodule?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`ConfigModule`** loads `.env` files into NestJS's DI container and exposes them through a singleton `ConfigService`. ```typescript ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', cache: true }) // Then inject anywhere: const host = this.configService.get<string>('DB_HOST', 'localhost'); ``` **Key:** set `isGlobal: true` once in `AppModule` and inject `ConfigService` anywhere. Add Joi `validationSchema` to catch missing vars at startup, not at runtime.Shown above the full answer for quick recall.Answer (EN)Image**`ConfigModule`** loads `.env` files into NestJS's dependency injection container and exposes them as a singleton `ConfigService` you can inject anywhere in your app. ## Theory ### TL;DR - ConfigModule reads `.env` at app bootstrap, parses key=value pairs, stores them in `ConfigService` - `isGlobal: true` means one import in `AppModule`, access everywhere without re-importing - `config.get<T>('KEY', defaultValue)` returns typed values with an optional fallback - Add a Joi schema to `validationSchema` and the app refuses to start if required vars are missing - `registerAs()` groups related config into namespaced objects like `database.host` ### Quick setup ```bash npm install @nestjs/config ``` ```typescript // app.module.ts import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // inject ConfigService without re-importing ConfigModule envFilePath: '.env', cache: true, // O(1) reads after first parse }), ], }) export class AppModule {} ``` That is the full setup. Now `ConfigService` is available in any injectable class across the entire application. ### Key difference from plain dotenv Manual `dotenv` pollutes `process.env` globally and gives you raw strings with no types, no defaults, no validation. `ConfigModule` wraps `dotenv` under the hood and puts the result into NestJS's DI container as a singleton. You get typed reads, default values, Joi validation at startup, and caching. No scattered `process.env` calls across every service file. ### When to use - DB credentials, API keys, port numbers: load once in `AppModule`, inject in services - Multi-environment setup: pass an array to `envFilePath`, NestJS picks the first file that exists - Apps following 12-factor principles: config lives outside the code, changes without a redeploy - Not for compile-time constants (use `const` for those) or standalone Node scripts outside NestJS (use dotenv directly there) ### How it works internally At app bootstrap, NestJS reads `envFilePath` (default `.env`), parses each `KEY=value` line using `dotenv`, and stores the result in `ConfigService`'s internal Map. When you call `config.get<T>('KEY')`, it looks up that Map, coerces the string to type `T`, and applies your default if the key is missing. With `cache: true`, the first read stores the result and every subsequent call skips the Map lookup. If you pass a `validationSchema`, the schema runs before the app starts. Any invalid or missing required value throws immediately and kills the process with a readable message. ### Typed, namespaced config with registerAs() Flat keys like `DB_HOST` and `DB_PORT` work fine for small apps. For anything larger, `registerAs()` groups related vars into objects: ```typescript // config/database.config.ts import { registerAs } from '@nestjs/config'; export default registerAs('database', () => ({ host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '5432', 10), name: process.env.DB_NAME || 'mydb', })); // app.module.ts ConfigModule.forRoot({ isGlobal: true, load: [databaseConfig] }) // any.service.ts const host = this.configService.get<string>('database.host'); const port = this.configService.get<number>('database.port'); ``` The dot notation in `get()` maps to nested objects created by `registerAs()`. Much cleaner than manually prefixing every flat key. ### Multiple .env files ```typescript ConfigModule.forRoot({ envFilePath: [ `.env.${process.env.NODE_ENV}.local`, `.env.${process.env.NODE_ENV}`, '.env.local', '.env', ], }) ``` NestJS processes the array left to right and stops once a key is found. A key in `.env.development.local` wins over the same key in `.env`. In production on Kubernetes, set `ignoreEnvFile: true` and let real environment variables from Secrets take over. ### Common mistakes **1. Forgetting `isGlobal: true`** ```typescript // Wrong: every module must import ConfigModule separately ConfigModule.forRoot() // not global // Fix: one call in AppModule, available everywhere ConfigModule.forRoot({ isGlobal: true }) ``` Without `isGlobal`, injecting `ConfigService` in a module that does not import `ConfigModule` throws a provider-not-found error. Easy to miss in unit tests where you bootstrap modules manually. **2. No validation, app crashes later** ```typescript // Wrong: returns NaN silently if DB_PORT='abc' in .env const port = this.configService.get<number>('DB_PORT'); // Fix: Joi schema, app refuses to start with a clear error message validationSchema: Joi.object({ DB_PORT: Joi.number().default(5432).required(), }) ``` Skipping validation is the single fastest way to get a 3am incident from a missing `JWT_SECRET` in a new environment. Joi catches it at startup with a readable message instead of a cryptic DB connection error 10 minutes into a deploy. **3. `config.get()` in constructor without `cache: true`** ```typescript // Wasteful in high-traffic apps without cache: true constructor(private config: ConfigService) { this.dbHost = config.get('DB_HOST'); // re-reads the Map on every instantiation } // Fix: set cache: true in forRoot() ConfigModule.forRoot({ isGlobal: true, cache: true }) ``` In most apps this does not matter. In microservices with high request volume where services get recreated often, it adds measurable overhead. **4. Hardcoding `.env` path for Docker/Kubernetes** ```typescript // Problem in production: K8s injects real env vars, no .env file exists envFilePath: '.env' // Fix: skip the file in production envFilePath: process.env.NODE_ENV !== 'production' ? '.env' : undefined // or simply: ignoreEnvFile: true when deploying to K8s ``` **5. Missing `abortEarly: false` in Joi options** By default Joi stops at the first validation error. If your `.env` is missing five variables, you fix them one by one across five restarts. Set `abortEarly: false` to see the full list at once. ### Real-world usage - TypeORM: `TypeOrmModule.forRootAsync({ useFactory: (c: ConfigService) => ({ host: c.get('DB_HOST') }) })` - Prisma: `new PrismaClient({ datasources: { db: { url: config.get('DATABASE_URL') } } })` - BullMQ: Redis URL from `ConfigService` in queue processor setup - Stripe / Twilio: API keys stored in `.env`, never hardcoded, read once in a constructor - Testing: use `ConfigModule.forRoot({ cache: false })` and set `process.env.KEY` between tests ### Follow-up questions **Q:** What happens if a required variable is missing and there is no validation? **A:** `config.get('KEY')` returns `undefined`. The app starts without errors. The crash surfaces later at the first DB query or API call, with a confusing message that points nowhere near the actual cause. **Q:** How does the array in `envFilePath` work? **A:** NestJS reads files left to right and stops once a key is found. Earlier entries in the array have higher priority. A key in `.env.development.local` overrides the same key in `.env`. **Q:** Does `config.get('DB.HOST')` work with a flat `.env` file? **A:** No. Dot notation maps to nested objects created by `registerAs()` or custom `load[]` factories. For a flat key you write `config.get('DB_HOST')`, not `config.get('DB.HOST')`. **Q:** How do you test code that depends on `ConfigService`? **A:** Two options. Pass `{ provide: ConfigService, useValue: { get: jest.fn().mockReturnValue('test-value') } }` directly in the testing module providers. Or use `ConfigModule.forRoot({ ignoreEnvFile: true, cache: false })` and set `process.env` variables before each test case. **Q:** In microservices with a shared config repo, how do you override config per service without a redeploy? **A:** Set `ignoreEnvFile: true` and inject configuration via Kubernetes ConfigMaps as real environment variables. For service-specific overrides, pass a custom factory to `load: [configurationFactory]` that reads a YAML file or calls Vault/AWS SSM. Restart the pod and the new config is live. ## Examples ### Basic: Reading DB credentials ```typescript // .env // DB_HOST=localhost // DB_PORT=5432 // DB_NAME=myapp // database.service.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @Injectable() export class DatabaseService { constructor(private config: ConfigService) {} getConnectionString(): string { const host = this.config.get<string>('DB_HOST', 'localhost'); // fallback if missing const port = this.config.get<number>('DB_PORT', 5432); const name = this.config.get<string>('DB_NAME'); return `postgres://${host}:${port}/${name}`; // Output: "postgres://localhost:5432/myapp" } } ``` `get<T>()` accepts a type parameter and a default value. If `DB_PORT` is absent from `.env`, the method returns `5432` rather than `undefined`. ### Intermediate: Joi validation + TypeORM integration ```typescript // app.module.ts import * as Joi from 'joi'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, validationSchema: Joi.object({ NODE_ENV: Joi.string().valid('development', 'production', 'test').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(), JWT_SECRET: Joi.string().min(32).required(), }), validationOptions: { abortEarly: false, // report all errors, not just the first }, }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: (config: ConfigService) => ({ type: 'postgres', host: config.get('DB_HOST'), port: config.get<number>('DB_PORT'), username: config.get('DB_USER'), password: config.get('DB_PASS'), database: config.get('DB_NAME'), autoLoadEntities: true, synchronize: config.get('NODE_ENV') !== 'production', }), inject: [ConfigService], }), ], }) export class AppModule {} ``` If `JWT_SECRET` is shorter than 32 characters or `DB_HOST` is missing, the app prints the full list of validation errors and exits before any request hits the server. ### Advanced: Namespaced config with registerAs() ```typescript // config/app.config.ts import { registerAs } from '@nestjs/config'; export default registerAs('app', () => ({ port: parseInt(process.env.PORT || '3000', 10), env: process.env.NODE_ENV || 'development', jwtSecret: process.env.JWT_SECRET, stripeKey: process.env.STRIPE_SECRET_KEY, })); // config/database.config.ts export default registerAs('database', () => ({ host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '5432', 10), name: process.env.DB_NAME || 'mydb', })); // app.module.ts ConfigModule.forRoot({ isGlobal: true, load: [appConfig, databaseConfig], // merge both into ConfigService }) // payment.service.ts @Injectable() export class PaymentService { private stripeKey: string; constructor(private config: ConfigService) { // dot-notation access to the namespaced 'app' object this.stripeKey = this.config.get<string>('app.stripeKey')!; } charge(amount: number) { console.log(`Charging with key: ${this.stripeKey.slice(0, 4)}****`); // Output: "Charging with key: sk_l****" } } ``` `registerAs()` separates concerns cleanly. The auth service reads from `app.*`, the DB service reads from `database.*`. Neither touches raw `process.env` directly, and neither knows about the other's configuration.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.