dotenv is the standard utility for loading environment variables from a .env file into process.env, enabling separation of secrets from code. config (node-config) provides a hierarchical configuration system that loads structured files (JSON, YAML, JS) based on the deployment environment, separate from environment variables. dotenv-expand extends dotenv by allowing variable substitution within .env files, reducing repetition. dotenv-flow enhances dotenv by automatically loading environment-specific files (like .env.development) based on NODE_ENV, simplifying multi-environment setups.
Managing settings across development, testing, and production is a core challenge in Node.js architecture. The packages config, dotenv, dotenv-expand, and dotenv-flow all address this problem but use different strategies. dotenv focuses on loading environment variables, config manages hierarchical application settings, while dotenv-expand and dotenv-flow extend the basic dotenv functionality. Let's compare how they handle loading, expansion, and environment switching.
dotenv loads variables from a .env file directly into process.env.
// dotenv: Load .env into process.env
require('dotenv').config();
console.log(process.env.DB_HOST);
config loads structured files from a config/ directory into a configuration object.
process.env by default.// config: Load from config/ directory
const config = require('config');
console.log(config.get('db.host'));
dotenv-expand modifies the result of dotenv before applying it.
.env file and expands variables in memory.process.env eventually.// dotenv-expand: Expand then load
const dotenv = require('dotenv');
const dotenvExpand = require('dotenv-expand');
dotenvExpand.expand(dotenv.config());
console.log(process.env.DB_URL);
dotenv-flow loads multiple files based on environment into process.env.
.env, .env.local, and environment-specific files.dotenv but with file hierarchy.// dotenv-flow: Load hierarchy into process.env
require('dotenv-flow').config();
console.log(process.env.DB_HOST);
dotenv does not support variable expansion out of the box.
// dotenv: No expansion support
// .env file:
// HOST=localhost
// PORT=3000
// URL=localhost:3000 (This will be literal text)
config supports variable expansion within configuration files.
// config: Expansion in config files
// config/default.js
module.exports = {
url: '${HOST}:${PORT}'
};
dotenv-expand adds expansion syntax to .env files.
${VAR} or $VAR syntax.dotenv.// dotenv-expand: Expansion in .env
// .env file:
// HOST=localhost
// PORT=3000
// URL=${HOST}:${PORT} (This resolves correctly)
dotenv-flow relies on dotenv rules unless paired with expand.
dotenv-expand for full support.// dotenv-flow: No expansion by default
// .env file:
// URL=${HOST}:${PORT} (Literal text without expand plugin)
dotenv requires manual file management for environments.
.env.development, .env.production.// dotenv: Manual path selection
require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` });
config has built-in hierarchical environment loading.
default.js then production.js.NODE_ENV.// config: Auto hierarchy
// config/default.js (loaded always)
// config/production.js (loaded if NODE_ENV=production)
const config = require('config');
dotenv-expand does not handle environment files.
// dotenv-expand: No file hierarchy
// Requires manual path logic like dotenv above
const dotenv = require('dotenv');
dotenvExpand.expand(dotenv.config({ path: '.env.prod' }));
dotenv-flow automates environment file loading.
.env, .env.local, .env.development, etc.// dotenv-flow: Auto hierarchy
// Loads .env + .env.development automatically
require('dotenv-flow').config();
dotenv uses the global process.env object.
// dotenv: Access via process.env
const port = process.env.PORT; // Returns string "3000"
config uses a dedicated configuration object.
config.get('key').// config: Access via config object
const port = config.get('server.port'); // Returns number 3000
dotenv-expand uses the global process.env object.
dotenv.// dotenv-expand: Access via process.env
const url = process.env.URL; // Returns string "localhost:3000"
dotenv-flow uses the global process.env object.
dotenv.// dotenv-flow: Access via process.env
const port = process.env.PORT; // Returns string "3000"
| Feature | dotenv | config | dotenv-expand | dotenv-flow |
|---|---|---|---|---|
| Primary Target | process.env | Config Object | process.env | process.env |
| File Formats | .env | JS, JSON, YAML | .env | .env |
| Variable Expansion | โ No | โ Yes | โ Yes | โ No (unless paired) |
| Env Hierarchy | โ Manual | โ Automatic | โ Manual | โ Automatic |
| Data Types | Strings | Typed (Num, Obj) | Strings | Strings |
While these tools have different goals, they share common ground in managing application settings.
.env or config files to git by convention.// All: .gitignore example
.env
.env.local
config/local.js
NODE_ENV to determine behavior.// All: Check environment
if (process.env.NODE_ENV === 'production') {
// Load secure settings
}
// All: One line setup
require('package-name').config();
dotenv is the foundational tool ๐งฑ โ perfect for simple projects needing basic secret loading. It is the default choice for most Node.js apps.
config is the structured alternative ๐๏ธ โ best for complex apps needing hierarchical, typed configuration separate from environment variables.
dotenv-expand is the enhancer ๐ โ use this when you need variable substitution within your .env files to reduce duplication.
dotenv-flow is the automator ๐ค โ ideal when you want multiple environment files loaded automatically without custom path logic.
Final Thought: For most modern applications, a combination works best. Use dotenv-flow (with dotenv-expand) for environment variables and secrets, and reserve config for complex, structured application settings that require validation and hierarchy. Choose based on whether you need simple strings or structured data.
Choose config when you need a hierarchical configuration system that supports multiple file formats (JSON, YAML, JS) and merges settings based on deployment environment. It is ideal for complex applications where configuration logic exceeds simple key-value pairs and requires structured data validation. Use this when you want to keep configuration separate from system environment variables.
Choose dotenv for the simplest scenario where you need to load a single .env file into process.env without extra features. It is the industry standard for basic secret management in local development and simple deployments. Use this when you do not need variable expansion or automatic environment-specific file loading.
Choose dotenv-expand if you need to reference other environment variables within your .env file to avoid repetition and maintain consistency. It works as a plugin to dotenv, enabling syntax like DB_URL=${HOST}:${PORT}. Use this when your configuration values depend on other variables defined in the same file.
Choose dotenv-flow if you want automatic loading of environment-specific files (like .env.development) based on NODE_ENV without writing custom logic. It simplifies managing different settings for local, test, and production environments seamlessly. Use this when you need a structured file hierarchy but prefer environment variables over structured configuration objects.
Node-config organizes hierarchical configurations for your app deployments.
It lets you define a set of default parameters, and extend them for different deployment environments (development, qa, staging, production, etc.).
Configurations are stored in configuration files within your application, and can be overridden and extended by environment variables, command line parameters, or external sources.
This gives your application a consistent configuration interface shared among a growing list of npm modules also using node-config.
The following examples are in JSON format, but configurations can be in other file formats.
Install in your app directory, and edit the default config file.
$ npm install config
$ mkdir config
$ vi config/default.json
{
// Customer module configs
"Customer": {
"dbConfig": {
"host": "localhost",
"port": 5984,
"dbName": "customers"
},
"credit": {
"initialLimit": 100,
// Set low for development
"initialDays": 1
}
}
}
Edit config overrides for production deployment:
$ vi config/production.json
{
"Customer": {
"dbConfig": {
"host": "prod-db-server"
},
"credit": {
"initialDays": 30
}
}
}
Use configs in your code:
const config = require('config');
//...
const dbConfig = config.get('Customer.dbConfig');
db.connect(dbConfig, ...);
if (config.has('optionalFeature.detail')) {
const detail = config.get('optionalFeature.detail');
//...
}
config.get() will throw an exception for undefined keys to help catch typos and missing values.
Use config.has() to test if a configuration value is defined.
Start your app server:
$ export NODE_ENV=production
$ node my-app.js
Running in this configuration, the port and dbName elements of dbConfig
will come from the default.json file, and the host element will
come from the production.json override file.
Type declarations are published under types/ and resolved via typesVersions. Subpath typings are included for config/async, config/defer, config/parser, config/raw, and config/lib/util in addition to the main config entrypoint.
If you still don't see what you are looking for, here are some more resources to check:
node-config contributors.May be freely distributed under the MIT license.
Copyright (c) 2010-2026 Loren West and other contributors