env-cmd vs config vs dotenv vs dotenv-safe
Managing Environment Configuration in Node.js Applications
env-cmdconfigdotenvdotenv-safeSimilar Packages:

Managing Environment Configuration in Node.js Applications

config, dotenv, dotenv-safe, and env-cmd are all widely used npm packages for managing environment-specific configuration in Node.js applications. They help separate sensitive or environment-dependent settings — such as API keys, database URLs, or feature flags — from application code. config uses hierarchical JSON or YAML files merged by environment name, providing a structured and immutable configuration object. dotenv loads variables from a .env file into process.env with no validation. dotenv-safe extends dotenv by requiring all keys defined in a .env.example file to be present, adding a safety net. env-cmd loads .env files via the command line before script execution, keeping configuration logic outside the application code itself.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
env-cmd1,778,0581,81455.2 kB26a year agoMIT
config06,426177 kB10a month agoMIT
dotenv020,501103 kB53 months agoBSD-2-Clause
dotenv-safe076810.4 kB42 years agoMIT

Managing Environment Configuration in Node.js: config vs dotenv vs dotenv-safe vs env-cmd

When building Node.js applications, managing environment-specific settings — like API keys, database URLs, or feature flags — is a foundational concern. The four packages under review (config, dotenv, dotenv-safe, and env-cmd) each tackle this problem differently, with distinct philosophies around source format, validation, runtime behavior, and deployment integration. Let’s examine how they work in practice and where each shines.

📁 Configuration Source: JSON Files vs .env Files vs CLI Overrides

config uses hierarchical JSON (or YAML/JS) files organized by environment name.

  • You create files like config/default.json, config/production.json, etc.
  • At runtime, it 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 your app
const config = require('config');
console.log(config.get('db.host')); // 'prod-db.example.com' if NODE_ENV=production

dotenv, dotenv-safe, and env-cmd all rely on .env files (key=value format).

  • .env is loaded into process.env at startup.
  • They differ mainly in validation and loading mechanics.
# .env
DB_HOST=localhost
DB_PORT=5432
API_TIMEOUT=5000
// With dotenv
require('dotenv').config();
console.log(process.env.DB_HOST); // 'localhost'

env-cmd loads .env files via CLI, not inside your code.

  • It wraps your script command to inject environment variables before your app starts.
  • Your application code never calls env-cmd directly.
# package.json
{
  "scripts": {
    "start:dev": "env-cmd -f .env.development node server.js",
    "start:prod": "env-cmd -f .env.production node server.js"
  }
}

🔒 Validation: Optional vs Required vs Built-In

config provides schema-like validation through custom logic or external tools, but doesn’t enforce required keys out of the box.

  • You can check for missing values manually:
const config = require('config');
if (!config.has('requiredKey')) {
  throw new Error('Missing required configuration: requiredKey');
}

dotenv has no validation — it silently ignores missing or malformed entries.

  • If a key is missing from .env, process.env.MY_KEY will be undefined.

dotenv-safe enforces presence of all keys listed in a .env.example file.

  • It compares .env against .env.example and throws if any expected variable is missing.
# .env.example (defines required keys)
DB_HOST=
API_KEY=
// Throws if .env is missing DB_HOST or API_KEY
require('dotenv-safe').config();

env-cmd supports validation via --strict mode (as of v10+).

  • When enabled, it ensures every key in the .env file has a non-empty value.
env-cmd --strict -f .env node app.js
# Fails if any value in .env is empty

🧪 Runtime Behavior: Process Mutation vs Immutable Config

dotenv, dotenv-safe, and env-cmd all mutate process.env.

  • Once loaded, your app reads from process.env.MY_VAR.
  • This makes them simple but ties config access to global state.
// After dotenv loads
const port = process.env.PORT || 3000;

config provides an immutable, namespaced config object.

  • You never touch process.env directly for app settings.
  • This enables better testing (you can stub the config module) and avoids polluting the global environment.
const config = require('config');
const port = config.get('server.port');

💡 Note: config can read from process.env for overrides (e.g., CUSTOMER_DB_HOST overrides config.customer.dbHost), but its primary interface is the config object.

🛠️ Deployment and CI/CD Integration

env-cmd excels in script-driven environments like CI pipelines or Docker setups where you want to avoid bundling config logic into your app.

  • Since it works at the CLI level, your application remains “dumb” about config sources.
  • Ideal when you manage multiple .env files per environment and don’t want conditional logic in code.

dotenv and dotenv-safe are best when you ship .env files with your app (common in development or simple deployments).

  • Not recommended for production if secrets are checked into version control — but often used with CI systems that inject .env at build time.

config suits complex applications with layered configuration (e.g., SaaS platforms with tenant-specific overrides).

  • Supports local overrides (local.json), instance-specific configs, and runtime extensibility.
  • Works well in containerized environments where config is mounted as volume files.

🔄 Hot Reloading and Dynamic Updates

None of these libraries support live config reloading out of the box.

  • config offers a util/watchedFile utility for manual file watching, but it’s not automatic.
  • For dynamic config, consider pairing any of these with a dedicated solution like consul, etcd, or cloud-based parameter stores.

🧩 Real-World Scenarios

Scenario 1: Simple Web App with Dev/Prod Environments

You’re building a Next.js or Express app and need different DB URLs for dev and prod.

  • Best choice: dotenv (for simplicity) or dotenv-safe (if you want safety).
  • Why? Low overhead, familiar .env format, and sufficient for basic needs.
// .env.development
DB_URL=postgresql://localhost/myapp_dev

// .env.production
DB_URL=$PROD_DB_URL  // injected by CI

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

Scenario 2: Enterprise Application with Nested Config

Your app has deep configuration trees (e.g., integrations.stripe.apiVersion, logging.transports.console.level).

  • Best choice: config
  • Why? Native support for nested objects, merging, and programmatic access without string parsing.
// config/default.json
{
  "integrations": {
    "stripe": {
      "apiVersion": "2023-10-16",
      "webhookSecret": "..."
    }
  }
}

const config = require('config');
const stripeConfig = config.get('integrations.stripe');

Scenario 3: CI Pipeline with Strict Env Requirements

Your GitHub Actions workflow must fail fast if a required environment variable is missing.

  • Best choice: dotenv-safe or env-cmd --strict
  • Why? Both enforce completeness, reducing runtime surprises.
# GitHub Actions
- name: Run tests
  run: npx env-cmd --strict -f .env.test npm test

Scenario 4: Dockerized Microservice

You deploy containers and inject config via mounted .env files or Kubernetes secrets.

  • Best choice: env-cmd (if using file mounts) or dotenv (if copying .env into image)
  • Why? Decouples config loading from app logic; env-cmd keeps the app unaware of config mechanics.

📌 Summary Table

PackageConfig FormatValidationRuntime AccessBest For
configJSON/YAML/JSManual/customconfig.get('x')Complex apps, nested config, enterprise use
dotenv.envNoneprocess.env.XSimple apps, quick setup, dev environments
dotenv-safe.envRequired keys (via .env.example)process.env.XSafety-critical apps, CI/CD pipelines
env-cmd.env--strict modeprocess.env.XScript-driven deploys, Docker, no-code loading

💡 Final Recommendation

  • Need structure, nesting, and programmatic control? → Use config.
  • Want the simplest possible setup for local development? → Use dotenv.
  • Require guaranteed presence of all environment variables? → Use dotenv-safe or env-cmd --strict.
  • Prefer keeping config logic outside your application code? → Use env-cmd.

All four are actively maintained and safe for production use — the right choice depends entirely on your team’s workflow, deployment strategy, and tolerance for runtime risk.

How to Choose: env-cmd vs config vs dotenv vs dotenv-safe

  • env-cmd:

    Choose env-cmd if you prefer managing environment configuration entirely outside your application code—such as in npm scripts, Dockerfiles, or CI workflows—and want to avoid bundling config-loading logic into your runtime. It’s especially useful when deploying to containerized or serverless environments.

  • config:

    Choose config if your application requires structured, hierarchical configuration with support for environment-specific overrides, nested objects, and programmatic access without mutating global state. It’s ideal for complex or enterprise-grade applications where configuration clarity, testability, and maintainability are critical.

  • dotenv:

    Choose dotenv if you need a lightweight, zero-config way to load environment variables from a .env file during development or simple deployments. It’s perfect for small to medium projects where validation isn’t a priority and you’re comfortable reading directly from process.env.

  • dotenv-safe:

    Choose dotenv-safe when you want the simplicity of .env files but require strict enforcement that all expected environment variables are defined—typically to prevent runtime errors in CI/CD pipelines or production. It’s a drop-in replacement for dotenv with added safety.

README for env-cmd

Linux Tests Windows Tests Coverage Status npm npm License Typescript-ESLint

env-cmd

A simple node program for executing commands using an environment from an env file.

💾 Install

npm install env-cmd or npm install -g env-cmd

⌨️ Basic Usage

Environment file ./.env

# This is a comment
ENV1=THANKS
ENV2=FOR ALL
ENV3=THE FISH

Package.json

{
  "scripts": {
    "test": "env-cmd -- mocha -R spec"
  }
}

Terminal

./node_modules/.bin/env-cmd -- node index.js

Using custom env file path

To use a custom env filename or path, pass the -f flag. This is a major breaking change from prior versions < 9.0.0

Terminal

./node_modules/.bin/env-cmd -f ./custom/path/.env -- node index.js

📜 Help

Usage: env-cmd [options] -- <command> [...args]

Options:
  -v, --version                 output the version number
  -e, --environments [envs...]  The rc file environment(s) to use
  -f, --file [path]             Custom env file path or .rc file path if '-e' used (default path: ./.env or ./.env-cmdrc.(js|cjs|mjs|json))
  -x, --expand-envs             Replace $var and ${var} in args and command with environment variables
  --recursive                   Replace $var and ${var} in env file with the referenced environment variable
  --fallback                    Fallback to default env file path, if custom env file path not found
  --no-override                 Do not override existing environment variables
  --silent                      Ignore any env-cmd errors and only fail on executed program failure.
  --use-shell                   Execute the command in a new shell with the given environment
  --verbose                     Print helpful debugging information
  -h, --help                    display help for command

🔬 Advanced Usage

.rc file usage

For more complex projects, a .env-cmdrc file can be defined in the root directory and supports as many environments as you want. Simply use the -e flag and provide which environments you wish to use from the .env-cmdrc file. Using multiple environment names will merge the environment variables together. Later environments overwrite earlier ones in the list if conflicting environment variables are found.

.rc file ./.env-cmdrc

{
  "development": {
    "ENV1": "Thanks",
    "ENV2": "For All"
  },
  "test": {
    "ENV1": "No Thanks",
    "ENV3": "!"
  },
  "production": {
    "ENV1": "The Fish"
  }
}

Terminal

./node_modules/.bin/env-cmd -e production -- node index.js
# Or for multiple environments (where `production` vars override `test` vars,
# but both are included)
./node_modules/.bin/env-cmd -e test,production -- node index.js

--no-override option

Prevents overriding of existing environment variables on process.env and within the current environment.

--fallback file usage option

If the .env file does not exist at the provided custom path, then use the default fallback location ./.env env file instead.

--use-shell

Executes the command within a new shell environment. This is useful if you want to string multiple commands together that share the same environment variables.

Terminal

./node_modules/.bin/env-cmd -f ./test/.env --use-shell -- "npm run lint && npm test"

Asynchronous env file support

EnvCmd supports reading from asynchronous .env files. Instead of using a .env file, pass in a .js file that exports either an object or a Promise resolving to an object ({ ENV_VAR_NAME: value, ... }). Asynchronous .rc files are also supported using .js file extension and resolving to an object with top level environment names ({ production: { ENV_VAR_NAME: value, ... } }).

Terminal

./node_modules/.bin/env-cmd -f ./async-file.js -- node index.js

-x expands vars in arguments

EnvCmd supports expanding $var values passed in as arguments to the command. The allows a user to provide arguments to a command that are based on environment variable values at runtime.

NOTE: You must escape the $ character with \ or your terminal might try to auto expand it before passing it to env-cmd.

Terminal

# $VAR will be expanded into the env value it contains at runtime
./node_modules/.bin/env-cmd -x -- node index.js --arg=\$VAR

or in package.json (use \\ to insert a literal backslash)

{
  "script": {
    "start": "env-cmd -x -- node index.js --arg=\\$VAR"
  }
}

--silent suppresses env-cmd errors

EnvCmd supports the --silent flag the suppresses all errors generated by env-cmd while leaving errors generated by the child process and cli signals still usable. This flag is primarily used to allow env-cmd to run in environments where the .env file might not be present, but still execute the child process without failing due to a missing file.

💿 Examples

You can find examples of how to use the various options above by visiting the examples repo env-cmd-examples.

💽️ Environment File Formats

These are the currently accepted environment file formats. If any other formats are desired please create an issue.

  • .env as key=value
  • .env.json Key/value pairs as JSON
  • .env.js JavaScript file exporting an object or a Promise that resolves to an object
  • .env-cmdrc as valid json or .env-cmdrc.json in execution directory with at least one environment { "dev": { "key1": "val1" } }
  • .env-cmdrc.js JavaScript file exporting an object or a Promise that resolves to an object that contains at least one environment

🗂 Path Rules

This lib attempts to follow standard bash path rules. The rules are as followed:

Home Directory = /Users/test

Working Directory = /Users/test/Development/app

TypeInput PathExpanded Path
Absolute/some/absolute/path.env/some/absolute/path.env
Home Directory with ~~/starts/on/homedir/path.env/Users/test/starts/on/homedir/path.env
Relative./some/relative/path.env or some/relative/path.env/Users/test/Development/app/some/relative/path.env
Relative with parent dir../some/relative/path.env/Users/test/Development/some/relative/path.env

🛠 API Usage

EnvCmd

A function that executes a given command in a new child process with the given environment and options

  • options { object }
    • command { string }: The command to execute (node, mocha, ...)
    • commandArgs { string[] }: List of arguments to pass to the command (['-R', 'Spec'])
    • envFile { object }
      • filePath { string }: Custom path to .env file to read from (defaults to: ./.env)
      • fallback { boolean }: Should fall back to default ./.env file if custom path does not exist
    • rc { object }
      • environments { string[] }: List of environment to read from the .rc file
      • filePath { string }: Custom path to the .rc file (defaults to: ./.env-cmdrc(|.js|.json))
    • options { object }
      • expandEnvs { boolean }: Expand $var values passed to commandArgs (default: false)
      • noOverride { boolean }: Prevent .env file vars from overriding existing process.env vars (default: false)
      • silent { boolean }: Ignore any errors thrown by env-cmd, used to ignore missing file errors (default: false)
      • useShell { boolean }: Runs command inside a new shell instance (default: false)
      • verbose { boolean }: Prints extra debug logs to console.info (default: false)
    • Returns { Promise<object> }: key is env var name and value is the env var value

GetEnvVars

A function that parses environment variables from a .env or a .rc file

  • options { object }
    • envFile { object }
      • filePath { string }: Custom path to .env file to read from (defaults to: ./.env)
      • fallback { boolean }: Should fall back to default ./.env file if custom path does not exist
    • rc { object }
      • environments { string[] }: List of environment to read from the .rc file
      • filePath { string }: Custom path to the .rc file (defaults to: ./.env-cmdrc(|.js|.json))
    • verbose { boolean }: Prints extra debug logs to console.info (default: false)
  • Returns { Promise<object> }: key is env var name and value is the env var value

🧙 Why

Because sometimes it is just too cumbersome passing a lot of environment variables to scripts. It is usually just easier to have a file with all the vars in them, especially for development and testing.

🚨Do not commit sensitive environment data to a public git repo! 🚨

🧬 Related Projects

cross-env - Cross platform setting of environment scripts

📋 Contributing Guide

I welcome all pull requests. Please make sure you add appropriate test cases for any features added. Before opening a PR please make sure to run the following scripts:

  • npm run lint checks for code errors and format according to ts-standard
  • npm test make sure all tests pass
  • npm run test-cover make sure the coverage has not decreased from current master