dotenv-expand vs config vs dotenv vs dotenv-safe
Environment Configuration Management in Node.js
dotenv-expandconfigdotenvdotenv-safeSimilar Packages:

Environment Configuration Management in Node.js

config, dotenv, dotenv-expand, and dotenv-safe are all Node.js packages designed to manage application configuration and environment variables, but they approach the problem differently. config provides a hierarchical, file-based configuration system that supports multiple environments through structured JSON, YAML, or JS files. dotenv loads environment variables from a .env file into process.env, offering a simple flat key-value approach. dotenv-expand extends dotenv by enabling variable interpolation within .env files (e.g., URL=http://${HOST}:${PORT}). dotenv-safe enhances dotenv with validation to ensure required environment variables are present, throwing errors if expected keys are missing. All four are commonly used in backend or full-stack JavaScript applications to separate configuration from code.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
dotenv-expand25,347,9341,056298 kB03 months agoBSD-3-Clause
config06,427159 kB152 months agoMIT
dotenv020,55046.1 kB14 days agoBSD-2-Clause
dotenv-safe076710.4 kB43 years agoMIT

Environment Configuration in Node.js: config vs dotenv vs dotenv-expand vs dotenv-safe

Managing environment-specific settings is a foundational concern in any production-grade application. The four packages under review — config, dotenv, dotenv-expand, and dotenv-safe — all aim to solve this problem but with fundamentally different philosophies, feature sets, and integration models. Let’s break down how they work, where they overlap, and when to use which.

🧩 Core Philosophy: File-Based Hierarchy vs Flat .env Simplicity

config assumes your app needs structured, hierarchical configuration that varies by deployment environment (development, test, staging, production). It loads .json, .yaml, or .js files from a dedicated config/ directory and merges them based on NODE_ENV.

// config/default.json
{
  "db": {
    "host": "localhost",
    "port": 5432
  },
  "apiTimeout": 5000
}

// config/production.json
{
  "db": {
    "host": "prod-db.example.com"
  }
}

// In code
const config = require('config');
console.log(config.get('db.host')); // 'prod-db.example.com' in production

dotenv, by contrast, embraces flat key-value pairs stored in a .env file. It simply parses the file and injects variables into process.env. There’s no hierarchy, merging logic, or built-in support for multiple environments beyond what you layer yourself.

# .env
DB_HOST=localhost
DB_PORT=5432
API_TIMEOUT=5000
// In code
require('dotenv').config();
console.log(process.env.DB_HOST); // 'localhost'

This difference is critical: config gives you structure out of the box; dotenv gives you simplicity and leaves structure up to you.

🔍 Variable Expansion: Injecting One Value into Another

Sometimes you need to compose values — for example, building a URL from a host and port. Neither config nor basic dotenv supports this natively.

dotenv-expand solves this by enabling variable interpolation within .env files, using syntax familiar from shell scripts.

# .env
HOST=localhost
PORT=3000
BASE_URL=http://${HOST}:${PORT}
// In code
const dotenv = require('dotenv');
const dotenvExpand = require('dotenv-expand');

const myEnv = dotenv.config();
dotenvExpand.expand(myEnv);

console.log(process.env.BASE_URL); // 'http://localhost:3000'

Note that dotenv-expand is not a standalone package — it’s designed to wrap the result of dotenv.config(). You always use it with dotenv.

config doesn’t support this kind of string interpolation directly. If you need computed values, you’d typically define them in a .js config file:

// config/default.js
const host = process.env.HOST || 'localhost';
const port = process.env.PORT || 3000;
module.exports = {
  baseUrl: `http://${host}:${port}`
};

So if your configuration logic lives in .env files and requires composition, dotenv + dotenv-expand is your path. If you’re already using JavaScript-based config files, config handles this naturally.

🛡️ Safety and Validation: Preventing Silent Failures

A missing environment variable can cause subtle bugs or crashes in production. Basic dotenv offers no safeguards — if a variable is missing, process.env.MY_VAR is just undefined.

dotenv-safe addresses this by allowing you to declare required variables via an .env.example (or custom) file. If any required variable is missing from your actual .env, it throws an error at startup.

# .env.example (defines required vars)
DB_HOST=
API_KEY=
# .env (must include all keys from .env.example)
DB_HOST=prod.example.com
API_KEY=secret123
// In code
require('dotenv-safe').config();
// Throws if .env is missing DB_HOST or API_KEY

config has a similar safety net via its util.validateConfig() method (though less commonly used), but more often, developers rely on runtime checks like config.has('required.key') or schema validation libraries (e.g., Joi) layered on top.

Neither dotenv nor dotenv-expand includes validation — that’s why dotenv-safe exists as a drop-in replacement for dotenv when safety matters.

📁 File Organization and Deployment Workflow

config expects a dedicated config/ directory with multiple files. This works well in traditional server deployments where you can manage filesystem layout, but it’s awkward in containerized or serverless environments where config is often injected via environment variables at runtime.

dotenv and its variants assume a single .env file (plus optional .env.local, etc.). This aligns better with 12-factor app principles, where config is strictly separated from code and passed via environment variables. Many CI/CD systems and platforms (like Vercel, Netlify, Heroku) expect this model.

If your team deploys via Docker or Kubernetes and injects secrets as env vars, dotenv may feel redundant — you might skip it entirely and read process.env directly. But during local development, .env files are invaluable for replicating production-like settings without hardcoding secrets.

🔗 Composability: How These Packages Work Together

It’s common to combine these tools rather than pick just one:

  • Use dotenv (or dotenv-safe) to load .env into process.env during local development.
  • Use config to build structured app settings from those environment variables.

Example:

// Load .env safely
require('dotenv-safe').config({
  allowEmptyValues: true,
  example: '.env.example'
});

// Then use config to organize settings
const appConfig = require('config');
// config/default.json can reference process.env values

And if you need variable expansion in .env, just add dotenv-expand:

const dotenv = require('dotenv');
const dotenvExpand = require('dotenv-expand');
const myEnv = dotenv.config();
dotenvExpand.expand(myEnv);

In fact, dotenv-safe and dotenv-expand are complementary, not competing — you can even use both:

const dotenvSafe = require('dotenv-safe');
const dotenvExpand = require('dotenv-expand');

const myEnv = dotenvSafe.config();
dotenvExpand.expand(myEnv);

This gives you validation and interpolation.

🚫 What None of These Do Well

All four packages are Node.js-only. They rely on filesystem access (fs.readFileSync) and won’t work in browser environments. For frontend apps, you typically inline environment variables at build time (e.g., via Webpack’s DefinePlugin or Vite’s import.meta.env).

Also, none provide type safety out of the box. You’ll still need TypeScript interfaces or runtime validation (e.g., Zod, Joi) to ensure config shapes are correct.

📊 Summary Table

PackagePrimary Use CaseHierarchical?Validates Required Vars?Supports Interpolation?Standalone?
configStructured, multi-env config files✅❌ (manual)❌✅
dotenvSimple .env → process.env❌❌❌✅
dotenv-expandAdd variable interpolation to dotenv❌❌✅❌ (needs dotenv)
dotenv-safeSafe dotenv with required var checking❌✅❌✅

💡 Practical Recommendations

  • For enterprise backends with complex config: Start with config. It scales well as your environment matrix grows.
  • For modern cloud-native apps (serverless, containers): Prefer dotenv-safe + dotenv-expand during local dev, but read process.env directly in production.
  • Never use plain dotenv in production code without validation — missing vars cause hard-to-debug failures.
  • Combine tools when needed: There’s no rule against using dotenv-safe to load env vars and config to structure them.

The right choice isn’t about which package is “best” — it’s about matching the tool to your team’s deployment strategy, config complexity, and tolerance for runtime errors.

How to Choose: dotenv-expand vs config vs dotenv vs dotenv-safe

  • dotenv-expand:

    Choose dotenv-expand when you’re already using dotenv and need to compose environment variables within your .env file using shell-like interpolation (e.g., DATABASE_URL=postgresql://${DB_HOST}:${DB_PORT}/mydb). Remember that it’s not a standalone package — it must be used alongside dotenv or dotenv-safe. Don’t use it if your configuration logic is better handled in JavaScript or if you don’t need variable composition.

  • config:

    Choose config when you need a structured, hierarchical configuration system that supports multiple environments (development, staging, production) through dedicated config files. It’s ideal for complex applications where settings are organized in nested objects and you want automatic merging based on NODE_ENV. However, avoid it if your deployment model relies solely on injected environment variables without filesystem access, such as in some serverless or containerized setups.

  • dotenv:

    Choose dotenv when you want a minimal, straightforward way to load environment variables from a .env file into process.env for local development. It’s perfect for simple applications or when you plan to manage configuration externally in production (e.g., via platform-provided env vars). Avoid it in production-critical paths without additional validation, as it provides no safeguards for missing or malformed variables.

  • dotenv-safe:

    Choose dotenv-safe when you need the simplicity of dotenv but require runtime validation to ensure all necessary environment variables are defined. It’s especially valuable in team environments or CI pipelines where missing configuration should fail fast rather than cause subtle bugs. Use it as a drop-in replacement for dotenv when safety is a priority, and consider pairing it with dotenv-expand if you also need variable interpolation.

README for dotenv-expand

If you have found dotenv-expand useful, consider checking out dotenvx for encrypting your .env files. Thank you for using dotenv. 🙏

dotenv-expand NPM version downloads

dotenv

Dotenv-expand is a zero-dependency module that adds variable expansion, command substitution, and encrypted value support on top of dotenv.

 

Usage

Install it alongside dotenv.

npm install dotenv dotenv-expand --save

Create a .env file in the root of your project:

# .env
USERNAME="dotenv"
DATABASE_URL="postgres://${USERNAME}@localhost/my_database"

As early as possible in your application, configure dotenv and expand its result:

// index.js
const dotenv = require('dotenv')
const dotenvExpand = require('dotenv-expand')

dotenvExpand.expand(dotenv.config())

console.log(process.env.DATABASE_URL)
$ node index.js
postgres://dotenv@localhost/my_database

That's it. process.env now contains the expanded values from your .env file.

 

Advanced

Preload

Consider using dotenvx instead. It includes expansion, command substitution, and encrypted .env support across languages and platforms:

dotenvx run -- node your_script.js

Use Node's --require (-r) option to load and expand .env before your application starts:

node -r dotenv-expand/config your_script.js

The preload entry is self-contained; you do not need to separately install dotenv.

Configuration options can be passed as command-line arguments:

node -r dotenv-expand/config your_script.js dotenv_config_path=/custom/path/to/.env

Or as environment variables:

DOTENV_CONFIG_ENCODING=latin1 node -r dotenv-expand/config your_script.js

Command-line arguments take precedence over environment variables.

pnpm
pnpm add dotenv dotenv-expand
Command Substitution

Use $(command) to replace an expression with the command's output:

NODE_VERSION=$(node --version)

Only use command substitution with .env files you trust. Commands run with the permissions of the current process.

Encrypted Values

Use dotenvx to encrypt your .env file. dotenv-expand automatically decrypts encrypted: values when DOTENV_PRIVATE_KEY is set:

DOTENV_PRIVATE_KEY="<private key>" node -r dotenv-expand/config your_script.js

Keep the private key separate from the encrypted .env file—for example, in your cloud platform's secrets manager. Read the dotenvx whitepaper for more details.

Custom Process Environment

Use processEnv to write expanded values to an object other than process.env:

const dotenvExpand = require('dotenv-expand')

const myEnv = {}

dotenvExpand.expand({
  processEnv: myEnv,
  parsed: {
    HELLO: 'World'
  }
})

console.log(myEnv.HELLO) // World
console.log(process.env.HELLO) // undefined
Interpolation Syntax

dotenv-expand supports braced and unbraced expansion, default values, and alternate values:

BASIC="basic"
BRACED=${BASIC}
UNBRACED=$BASIC
DEFAULT=${MISSING:-default}
ALTERNATE=${BASIC:+alternate}

See the complete interpolation rules.

 

FAQ

What rules does the expansion engine follow?

See a full list of rules here.

How does command substitution work?

Use $(command) in your .env file. dotenv-expand runs the command and replaces the expression with its output. See the Command Substitution example under Advanced.

How does encryption work?

Use dotenvx to encrypt your .env file. dotenv-expand automatically decrypts encrypted: values when DOTENV_PRIVATE_KEY is set.

DOTENV_PRIVATE_KEY="<private key>" node -r dotenv-expand/config your_script.js

Keep the private key separate from the encrypted .env file—for example, in your cloud platform's secrets manager. Read the dotenvx whitepaper for more details.

Is it safe to commit an encrypted `.env` file?

Yes, as long as the corresponding DOTENV_PRIVATE_KEY is stored separately and never committed with it. Use dotenvx to create and manage the encrypted file.

How can I avoid expanding pre-existing envs?

As of v12.0.0 dotenv-expand no longer expands process.env.

If you need this ability, use dotenvx and ship an encrypted .env file with your code, allowing safe expansion at runtime.

How can I override an existing environment variable?

Use dotenvx, as dotenv-expand does not support this.

dotenv-expand is a separate module (without knowledge of the loading of process.env and the .env file) and so cannot reliably know what to override.

 

Docs

Expand

expand expands, evaluates, and decrypts values in a dotenv result:

const dotenvExpand = require('dotenv-expand')

const result = dotenvExpand.expand({
  parsed: {
    BASIC: 'basic',
    BASIC_EXPAND: '${BASIC}',
    BASIC_EXPAND_SIMPLE: '$BASIC'
  }
})

console.log(result.parsed)

Options

processEnv

Default: process.env

Specify an object to read existing values from and write expanded values to.

parsed

The parsed key-value object to expand. This is normally the result of dotenv.config().

 

Contributing

See CONTRIBUTING.md.

CHANGELOG

See CHANGELOG.md.

Who's using dotenv-expand?

These npm modules depend on it.