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.
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.
config uses hierarchical JSON/YAML files stored in a config directory.
default.json, production.json) gets its own file.default → environment-specific → local 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.
.env file is not used in production; variables are set directly in the environment.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:
configassumes 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.
dotenv provides no validation.
// 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.
.env.example exists in .env (or the actual environment).# .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.
str(), num(), bool(), url(), etc.).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.
config.has('key') to check existence, but type checking is manual.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
dotenv is the simplest to start with — just npm install dotenv and call config().
dotenv-safe improves on this by failing fast if required variables are missing.
.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.
cleanEnv output can be used throughout the app, reducing scattered process.env calls.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.
config requires filesystem access to read config files.
config directory might not be bundled.dotenv, dotenv-safe, and envalid work anywhere process.env is available.
.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
config risks accidental exposure if config files are committed to version control.
dotenv and dotenv-safe encourage keeping .env out of Git (via .gitignore), but developers sometimes commit them by mistake.
process.env, which could leak secrets if process.env is logged.envalid mitigates this by returning a sanitized object that only includes declared variables.
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
| Feature | config | dotenv | dotenv-safe | envalid |
|---|---|---|---|---|
| Source | JSON/YAML files | .env file | .env file | Environment variables |
| Validation | None | None | Presence only | Full type validation |
| Type Coercion | No | No | No | Yes |
| Fail-Fast | No | No | Yes (missing vars) | Yes (missing/invalid) |
| 12-Factor Compliant | ❌ | ✅ | ✅ | ✅ |
| Serverless Friendly | ❌ | ✅ | ✅ | ✅ |
| Secrets Safety | Risky (files) | Medium | Medium | High (sanitized) |
config gives you structure and inheritance.dotenv gets you up and running in seconds.dotenv-safe enforces a contract via .env.example.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.
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.
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.
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.
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.
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