config vs envalid vs dotenv-safe vs dotenv
Environment Configuration Management in Node.js Applications
configenvaliddotenv-safedotenvSimilar Packages:

Environment Configuration Management in Node.js Applications

config, dotenv, dotenv-safe, and envalid are all npm packages designed to help manage application configuration, primarily by loading environment variables or external configuration files. They address the common need to separate configuration from code, support different environments (development, staging, production), and ensure that required settings are present and valid. While dotenv and its variants focus on loading .env files into process.env, config uses hierarchical JSON/YAML files, and envalid emphasizes runtime validation and type safety of environment variables.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
config1,520,3016,424207 kB14a month agoMIT
envalid560,6261,55184.4 kB125 months agoMIT
dotenv-safe232,81077110.4 kB42 years agoMIT
dotenv020,34693.3 kB9a month agoBSD-2-Clause

Managing Environment Configuration: config vs dotenv vs dotenv-safe vs envalid

When building Node.js applications, managing configuration across environments is a recurring challenge. The four packages — config, dotenv, dotenv-safe, and envalid — offer different philosophies for handling this problem. Let’s compare them based on how they load settings, validate inputs, and fit into real-world deployment scenarios.

📁 Source of Truth: Files vs Environment Variables

config uses hierarchical JSON/YAML files stored in a config directory.

  • Each environment (e.g., default.json, production.json) gets its own file.
  • Values are merged: defaultenvironment-specificlocal overrides.
// config/default.json
{ "port": 3000, "db": { "host": "localhost" } }

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

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

dotenv, dotenv-safe, and envalid all rely on environment variables, typically loaded from a .env file during development.

  • The .env file is not used in production; variables are set directly in the environment.
  • All three inject values into process.env (though envalid discourages direct access afterward).
# .env
PORT=3000
DB_HOST=localhost
// dotenv / dotenv-safe
require('dotenv').config();
console.log(process.env.PORT); // '3000'

// envalid
const { cleanEnv, num, str } = require('envalid');
const env = cleanEnv(process.env, {
  PORT: num(),
  DB_HOST: str()
});
console.log(env.PORT); // 3000 (as a number)

💡 Key difference: config assumes filesystem access; the others assume environment variables are the primary source, which aligns better with 12-factor app principles and works in serverless/edge runtimes.

✅ Validation and Type Safety

dotenv provides no validation.

  • If a variable is missing or malformed, your app may crash later with a confusing error.
// dotenv – no safety net
require('dotenv').config();
const port = process.env.PORT; // Could be undefined or 'abc'
server.listen(port); // Might fail silently or throw later

dotenv-safe adds presence checking against a .env.example file.

  • It ensures every variable in .env.example exists in .env (or the actual environment).
  • But it doesn’t validate types or formats.
# .env.example
PORT=
DB_HOST=
// dotenv-safe
require('dotenv-safe').config(); // Throws if PORT or DB_HOST missing
const port = parseInt(process.env.PORT, 10); // Still need manual parsing

envalid offers declarative validation with type coercion.

  • You define expected variables with built-in validators (str(), num(), bool(), url(), etc.).
  • Missing or invalid values throw descriptive errors at startup.
  • Returns a clean object with correctly typed values.
const { cleanEnv, num, str } = require('envalid');
const env = cleanEnv(process.env, {
  PORT: num({ default: 3000 }),
  DB_HOST: str()
});
// env.PORT is guaranteed to be a number; env.DB_HOST a string

config has no built-in validation.

  • You can use config.has('key') to check existence, but type checking is manual.
  • Errors often surface only when the app tries to use a misconfigured value.
const config = require('config');
if (!config.has('db.host')) {
  throw new Error('Missing db.host');
}
// Still no guarantee it's a string or valid host

🛠️ Developer Experience and Debugging

dotenv is the simplest to start with — just npm install dotenv and call config().

  • Great for prototypes or internal tools where robustness isn’t critical.

dotenv-safe improves on this by failing fast if required variables are missing.

  • The .env.example file serves as living documentation of expected variables.

envalid provides the clearest error messages and encourages a single source of truth for config access.

  • Its cleanEnv output can be used throughout the app, reducing scattered process.env calls.
  • Works well with TypeScript via JSDoc or ambient types.

config offers powerful features like custom environment variables (CUSTOMER_ENV) and config watching, but its file-based approach can confuse developers expecting 12-factor compliance.

  • Debugging merge order (default → env → local) can be tricky without logging.

🌐 Deployment and Runtime Constraints

config requires filesystem access to read config files.

  • This breaks in serverless environments (AWS Lambda, Vercel Edge Functions) or Docker containers where the config directory might not be bundled.
  • Not suitable for frontend or edge runtimes.

dotenv, dotenv-safe, and envalid work anywhere process.env is available.

  • They’re compatible with serverless, Docker, and PaaS deployments (Heroku, Render, etc.).
  • In production, you skip .env files entirely and set variables via platform UI or CLI.
// All three work the same in production:
// No .env file needed — variables come from the environment

🔒 Security Considerations

config risks accidental exposure if config files are committed to version control.

  • Sensitive data (passwords, keys) should never be in config files unless encrypted.

dotenv and dotenv-safe encourage keeping .env out of Git (via .gitignore), but developers sometimes commit them by mistake.

  • Both load all variables into process.env, which could leak secrets if process.env is logged.

envalid mitigates this by returning a sanitized object that only includes declared variables.

  • You can avoid referencing process.env directly after initialization, reducing accidental leakage.
// envalid – only PORT and DB_HOST are exposed
const env = cleanEnv(process.env, { PORT: num(), DB_HOST: str() });
// process.env may contain AWS_SECRET_KEY, but env does not

📊 Summary Table

Featureconfigdotenvdotenv-safeenvalid
SourceJSON/YAML files.env file.env fileEnvironment variables
ValidationNoneNonePresence onlyFull type validation
Type CoercionNoNoNoYes
Fail-FastNoNoYes (missing vars)Yes (missing/invalid)
12-Factor Compliant
Serverless Friendly
Secrets SafetyRisky (files)MediumMediumHigh (sanitized)

💡 When to Use Which

  • Building a traditional Express app with full filesystem access?config gives you structure and inheritance.
  • Starting a quick prototype or internal tool?dotenv gets you up and running in seconds.
  • Working on a team and want to avoid missing env vars?dotenv-safe enforces a contract via .env.example.
  • Shipping a production app where config correctness is critical?envalid’s validation and type safety are worth the slight setup overhead.

In modern cloud-native development, envalid often strikes the best balance between safety, clarity, and compatibility — especially as teams adopt TypeScript and strict CI/CD practices. But for simpler cases, dotenv-safe offers a lightweight middle ground between raw dotenv and full validation.

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

  • config:

    Choose config if your application requires complex, hierarchical configuration that varies across multiple environments (e.g., development, test, production) and you prefer structured JSON or YAML files over environment variables. It’s well-suited for traditional server-side Node.js applications where filesystem access is available and you want fine-grained control over config inheritance and overrides. However, avoid it in serverless or edge environments where file system access is restricted or unavailable.

  • envalid:

    Choose envalid when you need strong guarantees about the presence, type, and correctness of environment variables at runtime. It provides declarative validation with built-in parsers (e.g., str(), num(), bool()), throws clear errors for invalid or missing values, and returns a sanitized, typed object that replaces direct process.env access. It’s especially valuable in production systems, CI/CD pipelines, or TypeScript projects where type safety and fail-fast behavior are priorities.

  • dotenv-safe:

    Choose dotenv-safe if you want the simplicity of dotenv but require a safety net to ensure critical environment variables are defined. It enforces the presence of all variables listed in a .env.example file, throwing an error at startup if any are missing. This is useful for team environments where you want to prevent accidental omissions of required settings without adding complex validation logic.

  • dotenv:

    Choose dotenv when you need a simple, zero-configuration way to load key-value pairs from a .env file into process.env. It’s ideal for small to medium projects where you control the deployment environment and can ensure .env files are present during development. Since it performs no validation, it’s best paired with manual checks or used in contexts where missing or malformed variables are acceptable during early development.

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