config vs dotenv vs dotenv-cli vs dotenv-safe
Environment Configuration Management in Node.js
configdotenvdotenv-clidotenv-safeSimilar Packages:

Environment Configuration Management in Node.js

config, dotenv, dotenv-cli, and dotenv-safe are all npm packages designed to manage environment-specific configuration in Node.js applications, but they take different approaches. config uses structured JSON/YAML files per environment and supports hierarchical settings. dotenv loads a flat .env file of key-value pairs into process.env. dotenv-cli enables running commands with .env variables injected without modifying application code. dotenv-safe extends dotenv by enforcing that all required variables (defined in .env.example) are present in .env, failing fast if any are missing.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
config06,423207 kB13a month agoMIT
dotenv020,35293.3 kB112 months agoBSD-2-Clause
dotenv-cli059110.9 kB25 months agoMIT
dotenv-safe077110.4 kB42 years agoMIT

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

Managing environment-specific settings is a foundational concern in any Node.js application. The four packages under review — config, dotenv, dotenv-cli, and dotenv-safe — each approach this problem differently, with distinct philosophies around structure, safety, and developer workflow. Let’s examine how they work in practice and where each shines.

📁 Configuration Philosophy: Structured Files vs Flat .env

config assumes your app uses structured JSON or YAML files per environment (e.g., config/development.json, config/production.json). It merges defaults with environment overrides and provides a consistent API to access values.

// config: structured file per environment
// config/default.json
{ "db": { "host": "localhost" } }

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

// In code
const config = require('config');
console.log(config.get('db.host')); // Picks correct host based on NODE_ENV

dotenv, by contrast, loads a flat .env file of key-value pairs into process.env. There’s no nesting or merging — just simple string assignments.

// .env
DB_HOST=localhost
API_KEY=secret123

// In code
require('dotenv').config();
console.log(process.env.DB_HOST); // 'localhost'

dotenv-cli doesn’t load variables into your app directly. Instead, it lets you run commands with .env values injected into the environment — useful for scripts or one-off tasks.

# dotenv-cli: run a command with .env loaded
npx dotenv -e .env node migrate.js

dotenv-safe works like dotenv but enforces required variables by comparing against an .env.example file. If a required key is missing, it throws an error at startup.

// .env.example (defines required keys)
DB_HOST=
API_KEY=

// .env (must include all keys from .env.example)
DB_HOST=localhost
API_KEY=secret123

// In code
require('dotenv-safe').config();
// Throws if .env is missing DB_HOST or API_KEY

🔒 Safety and Validation: Fail-Fast vs Silent Defaults

config validates structure at runtime using config.util.loadFileConfigs(), but won’t prevent missing keys unless you explicitly check with has() or use TypeScript definitions.

const config = require('config');
if (!config.has('requiredService.url')) {
  throw new Error('Missing requiredService.url');
}

dotenv offers no validation. If a key is missing from .env, process.env.MY_VAR will simply be undefined — which may cause subtle bugs later.

require('dotenv').config();
// No error if PORT is missing; app might crash later
const port = process.env.PORT || 3000;

dotenv-cli inherits dotenv’s lack of validation since it just wraps the same loading logic for CLI use.

# If .env is missing PORT, the script sees undefined
npx dotenv -e .env node server.js

dotenv-safe is built for fail-fast safety. It compares your .env against .env.example and exits immediately if required variables are absent.

// This will throw if .env lacks any key from .env.example
require('dotenv-safe').config({
  allowEmptyValues: false // default behavior
});

🛠️ Developer Experience: Workflow Integration

config encourages a configuration-as-code approach. You commit all config files (with placeholders for secrets) and override only what’s needed per environment. This works well in containerized or PaaS environments where config is injected via files.

dotenv fits naturally into local development workflows where developers copy .env.example to .env and fill in values. It’s simple and widely understood, but requires discipline to avoid committing real secrets.

dotenv-cli shines when you need to run ad-hoc commands (tests, migrations, scripts) with environment variables without modifying your app’s startup code.

// package.json
{
  "scripts": {
    "migrate": "dotenv -e .env node ./scripts/migrate.js"
  }
}

dotenv-safe adds a safety net to the .env workflow by ensuring your local or CI environment isn’t missing critical variables before the app even starts.

🌐 Real-World Usage Scenarios

Scenario 1: Enterprise Application with Multiple Environments

You deploy to dev, staging, and prod, each with complex nested settings (database clusters, feature flags, service endpoints).

  • Best choice: config
  • Why? Structured files support deep nesting, inheritance, and clear audit trails. Secrets can be injected separately (e.g., via Kubernetes secrets mounted as files).

Scenario 2: Small Team Building a Startup MVP

You want something dead simple for local dev and Heroku-style env var deployment.

  • Best choice: dotenv
  • Why? Minimal setup, zero learning curve, and Heroku already uses process.env natively.

Scenario 3: Running Database Migrations in CI

Your CI pipeline needs to run a migration script with database credentials, but you don’t want to modify the script itself.

  • Best choice: dotenv-cli
  • Why? Lets you inject .env values into any command without touching application code.
# In CI script
npx dotenv -e .env.staging node ./migrate.js

Scenario 4: Strict Compliance Requirements

Your app must fail immediately if any required configuration is missing (e.g., financial or healthcare apps).

  • Best choice: dotenv-safe
  • Why? Guarantees that missing variables halt startup, preventing partial or unsafe execution.

⚖️ Key Trade-offs Summarized

PackageStructureValidationBest For
configNested filesManualComplex, multi-env applications
dotenvFlat .envNoneSimple apps, rapid prototyping
dotenv-cliFlat .envNoneCLI scripts and one-off commands
dotenv-safeFlat .envRequired keysSafety-critical local/CI setups

💡 Final Guidance

  • If your configuration is hierarchical or environment-rich, go with config.
  • If you’re building a small-to-medium app and prefer the .env convention, use dotenv — but add runtime checks for critical variables.
  • If you need to run external commands with environment variables, reach for dotenv-cli.
  • If you can’t afford missing variables in development or CI, dotenv-safe gives you peace of mind with minimal overhead.

All four tools are actively maintained and solve real problems — the right choice depends entirely on your team’s workflow, deployment strategy, and risk tolerance.

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

  • config:

    Choose config if your application requires structured, hierarchical configuration across multiple environments (e.g., development, staging, production) with support for nested settings and file-based overrides. It’s ideal for enterprise or complex applications where configuration is managed as code and secrets are injected separately via infrastructure.

  • dotenv:

    Choose dotenv if you need a simple, lightweight solution for loading environment variables from a .env file into process.env, especially for small-to-medium projects or rapid prototyping. It works well with platforms like Heroku that natively use environment variables, but requires manual validation for required keys.

  • dotenv-cli:

    Choose dotenv-cli when you need to run shell commands, scripts, or one-off tasks (like database migrations or tests) with environment variables loaded from a .env file without altering your application’s startup logic. It’s perfect for CI pipelines or developer tooling where config injection should be external to the app.

  • dotenv-safe:

    Choose dotenv-safe if you require strict validation that all necessary environment variables are defined before your application starts, using an .env.example file as a contract. It’s best suited for safety-critical applications or teams that want to catch missing configuration early in development or CI environments.

README for config

Configure your Node.js Applications

npm package Downloads Issues

Release Notes

Introduction

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.

Project Guidelines

  • Simple - Get started fast
  • Powerful - For multi-node enterprise deployment
  • Flexible - Supporting multiple config file formats
  • Lightweight - Small file and memory footprint
  • Predictable - Well tested foundation for module and app developers

Quick Start

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.

TypeScript

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.

Articles

Further Information

If you still don't see what you are looking for, here are some more resources to check:

Contributors

lorenwestjdmarshallmarkstosi­Moseselliotttfmdkitzman
jfelegeleachi­M2kjosxenyoleosuncinarthanzel
leonardovillelajeremy-daley-krsimon-scherzingerBadger­Badger­Badger­Badgernsaboviccunneen
Osterjourth507tiny-rac00neheikesfgheorgheroncli
superovenairdrummingfoolwmertensXadilla­Xinsidedsbert

License

May be freely distributed under the MIT license.

Copyright (c) 2010-2026 Loren West and other contributors