Every Node.js project starts the same way: a .env file appears in the repo root, process.env reads spread across the codebase, and nobody can remember which variables are required. Then a new developer joins, runs the app, and gets a cryptic undefined where a database URL should be — at 2 a.m., in production. Configuration management is the least glamorous and most expensive part of shipping software, and the library you choose determines how much of that pain you feel.
This guide compares the three config libraries that dominate the Node.js ecosystem: dotenv (20,521 stars), envalid (1,586 stars), and node-config (6,428 stars). All three are actively maintained as of August 2026, and each solves a different layer of the problem.
TL;DR — Quick Verdict
If you just need .env files loaded into process.env, use dotenv — it is the industry default, and 12-factor apps built on it are portable by definition. If you want validated, typed environment variables that fail fast on missing values, use envalid — it is the smallest library that catches configuration errors before they become runtime errors. If you need hierarchical, environment-aware config files (defaults + production overrides + local overrides), use node-config — it is the only one of the three designed for multi-environment deployment scenarios like Docker and Kubernetes. These are complementary, not competitors: envalid and node-config both load .env files via dotenv under the hood.
Quick Comparison Table
| Dimension | dotenv | envalid | node-config |
|---|---|---|---|
| GitHub Stars | 20,521 | 1,586 | 6,428 |
| Last Commit (Aug 2026) | 2026-08-04 | 2026-08-21 | 2026-08-18 |
| License | BSD-2-Clause | MIT | MIT |
| Core job | Load .env into process.env | Validate & type env vars | Hierarchical config files |
| Config sources | .env file | process.env | JSON/JS/YAML files + env overrides |
| Type checking | None | Yes (str, num, bool, email, url, json) | Light (via js-yaml / no runtime types) |
| Validation / fail-fast | No | Yes — crashes on missing/invalid | No built-in schema validation |
| Defaults support | No | Yes (per-var default) | Yes (default.json + env-specific files) |
| Environment switching | Manual | Via NODE_ENV | Automatic (NODE_ENV or NODE_CONFIG_ENV) |
| Secrets support | Yes (plain env vars) | Yes (env vars) | Yes (env overrides, config.get) |
| Bundle size | ~3 kB | ~5 kB | ~30 kB (with js-yaml) |
| Best suited for | Any Node app needing env files | Services that must fail fast | Multi-environment deployments |
Decision Matrix — Pick in 10 Seconds
| Use Case | Recommended Library | Why |
|---|---|---|
Load a .env file into a script or app | dotenv | One line, zero config, industry standard |
| Catch missing config at startup, not at runtime | envalid | Typed schema + hard crash on invalid env |
| Microservices with per-environment config files | node-config | default.json + production.json + local.json layering |
| Docker/Kubernetes deployments with env injection | envalid or node-config | Env-based overrides work in both; validate with envalid |
| CLI tools and small scripts | dotenv | Minimal footprint, no file hierarchy to maintain |
| Team of 10+ with shared config conventions | node-config | Centralized file structure, deployment docs, mature tooling |
dotenv — The One-Line Standard
dotenv is the most-installed configuration package in the Node ecosystem. It reads a .env file at the project root and merges its KEY=VALUE pairs into process.env — nothing more, nothing less. Its genius is that it makes the 12-factor “store config in the environment” pattern work locally without any infrastructure.
| |
| |
The entire API is that .config() call (plus dotenv.parse() if you need to read a file manually). Where it shines: portability — the same .env works in CI, on a laptop, and in a container with --env-file. Where it struggles: nothing is validated or typed. A missing variable silently becomes undefined, and a typo like PORT=abc flows straight into your HTTP server as a NaN that only fails on the first connection.
envalid — Fail Fast, Fail Loud
envalid takes the environment-variable approach and adds the missing safety net: a declarative schema that validates and coerces every variable at startup, crashing the process with a helpful error message if anything is missing or malformed. It works in Node, Bun, and any compatible JS runtime, and it ships with common validators plus a custom-validator hook.
| |
If DB_URL is missing, the process exits with:
| |
Where it shines: type coercion — num() converts "8080" to 8080, bool() parses "true"/"1"/"yes", and invalid values produce precise errors instead of silent NaNs. Where it struggles: it only reads process.env — no config file hierarchy. For a handful of services that is a feature, not a bug; for complex deployments you layer it on top of node-config or a config file loader.
node-config — Hierarchical Config for Real Deployments
node-config organizes configuration as a file hierarchy: default.json holds baseline values, and environment-specific files (production.json, development.json, test.json) override them based on NODE_ENV. A local.json (git-ignored) layer lets individual developers override locally without touching shared files.
| |
| |
| |
Where it shines: deployment-aware layering, deep-merge semantics, and a mature ecosystem — it has been in production for over a decade and supports JSON, JS, and YAML config files. Environment variables can override individual keys too, which keeps containers and Kubernetes manifests happy. Where it struggles: no runtime validation — a typo in a key returns a loud ConfigError on .get() (good), but type mistakes in files are only caught when consumed. Pair it with envalid for env vars and a lightweight schema check for the rest.
Pitfalls & Migration Notes
- Never commit real secrets in
.env. dotenv’s own docs recommend keeping.envout of git (.env.examplein,.envout).githistory is forever — a leakedAWS_SECRET_ACCESS_KEYrequires rotation even after deletion. dotenvdoes not override existing environment variables by default. In CI or containers, injected vars win over the.envfile. If you need the opposite, usedotenv.config({ override: true })— but that surprises teams expecting standard behavior, so document it loudly.- Type coercion is where config bugs hide.
"PORT=abc"becomesNaNin dotenv and node-config, but envalid rejects it at startup. If you stay on dotenv, add a runtime schema check at the service boundary — or migrate the critical vars to envalid incrementally. - node-config freezes its config object. The returned object is deep-frozen by default (
config.util.toObject()to unfreeze). Mutating config at runtime — a common quick hack — throws in strict mode and silently does nothing otherwise. Set values at startup, never mid-request. NODE_ENVis not the only environment axis. node-config defaults toNODE_ENV(orNODE_CONFIG_ENV), but real deployments often needCUSTOM_NODE_ENVfor staging variants. Configure it explicitly in your orchestrator or every environment loadsdefault.jsonpluslocal.json.- Loading order matters. If you call
dotenv.config()after modules have already readprocess.envat import time, those modules seeundefined. Load dotenv as the very first line of your entry point — before any other require/import — or use--env-file=.envwith modern Node (18.20+/20.6+). - Secrets vs config. Config files in git should contain zero secrets. Put credentials in environment variables (via Docker
env_file, Kubernetes secrets, or your CI vault) and reference them with envalid or node-config’s env override — not indefault.json. - Bundle size for edge runtimes. If you deploy to serverless or edge runtimes, dotenv (~3 kB) and envalid (~5 kB) stay lean; node-config pulls in js-yaml and other deps. For edge, prefer envalid plus a tiny YAML/JSON loader.
FAQ
Can I use dotenv and envalid together?
Yes, and it is a common pattern: dotenv.config() first, then cleanEnv(process.env, schema) — dotenv loads the file, envalid validates what landed in the environment.
Does node-config support environment variables?
Yes. Any key can be overridden with NODE_CONFIG (JSON string) or individual CONFIG__key__path variables, and NODE_CONFIG_ENV picks the config file set. This is how Kubernetes and Docker inject overrides.
Which library is best for a monorepo?
dotenv or envalid per package, with a shared schema package for validated variables. node-config’s file hierarchy becomes awkward across many packages because each package needs its own config/ directory.
Is envalid still maintained? Yes — the repository is actively maintained (last push 2026-08-21) and supports Node, Bun, and Deno-compatible runtimes via its small, dependency-free core.
How do I handle config in TypeScript projects?
All three ship types. envalid infers a fully typed env object from the schema — the best developer experience. dotenv and node-config expose typed get() helpers but leave the heavy lifting to your own types.
Should I migrate from dotenv to envalid?
If missing configuration has burned you in production, yes — it is a drop-in improvement: keep dotenv for loading, add an envalid schema for validation. The migration is incremental and safe because envalid reads the same process.env you already populate.
Why Config Discipline Pays Off
Configuration bugs are uniquely nasty because they surface late — the app boots fine on your laptop and fails only in the production environment with real traffic. The libraries above each remove one layer of that risk: dotenv makes local/cloud parity trivial, envalid converts missing config into an immediate, readable error, and node-config gives multi-environment deployments a structure humans can audit. None of them require a heavyweight framework, and all three are permissively licensed and free.
For the broader tooling picture, our development environment managers comparison covers mise vs asdf vs direnv for runtime versions, and the cross-language configuration libraries guide shows how Viper, koanf, and TypeSafe Config solve the same problem in Go and the JVM. If your config lives in environment variables in Go specifically, the Go env config libraries article is the direct counterpart.
Bottom line: load with dotenv, validate with envalid, and reach for node-config only when your deployment matrix genuinely needs file-based layering. Your future on-call self will thank you.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com