Why Config Goes to Environment Variables Instead of Hardcoding
Config is the part that changes behavior per environment (local, test, production) without changing code: database URLs, API endpoints, secrets, log levels. Hardcoding them means every environment change requires code edits and redeploys, and risks shipping a test DB address into production.
The 12-factor principle requires strict separation of config storage from code. Environment variables are the most common, framework-agnostic carrier — read uniformly whether you use Spring, Django or Node — and they support runtime injection, so switching environments needs no image rebuild.
The cost is management: once config scatters across hundreds of keys, naming, defaults and required-or-not become a maintenance burden, so you need tooling conventions as a safety net — which the following sections cover.
# 12 因素:同一份代码,靠环境变量区分行为
export DATABASE_URL="mysql://app:***@prod-db:3306/shop"
export LOG_LEVEL="info"
# 代码里不再出现连接串硬编码,只读环境变量Environment Variable Precedence: Don’t Be Fooled by Stale Values in .env
Same-name variables come from many places: system env, shell exports, .env files, .env.local, Docker -e, CI secrets. Who overrides whom is confusing, and the classic dotenv trap is "I changed .env but it seems to have no effect".
With Node’s dotenv, the default is not to override an existing system variable: if the process env already holds the same name, .env will not replace it. The consequence: you exported an old value in your local shell, and no matter how you edit .env it feels like nothing changed.
To be crystal clear, learn the general precedence: in-process explicit assignment > shell env > .env.local > .env > defaults. When a change "does not take", echo $KEY to see what the process actually resolved, then decide whether to override or delete the stale one.
# 查看当前生效值(字段解析,别靠猜)
printenv | grep -E '^(DATABASE_URL|NODE_ENV) ='
# Node 默认不覆盖已存在变量
# 强制覆盖需显式声明
require('dotenv').config({ override: true })Set Rules with .env.example and Block Secrets with .gitignore
What should actually be committed is the template of config, not the values. Create .env.example or .env.sample listing every key with its purpose and whether it is required, using placeholders for real values. A new member clones, copies it to .env and fills local values — standardizing names without dragging secrets into the repo.
Also make sure to put the real .env in .gitignore. If .env ever appeared in a historical commit, gitignore alone cannot erase it — you must rewrite history (filter-branch or BFG) and force-push, then immediately rotate the leaked secrets. Such incidents cost far more than prevention.
# .env.example(提交)
DATABASE_URL=mysql://user:pass@localhost:3306/shop
API_KEY=change-me
# .gitignore(追加)
.env
.env.local
.env.*.localSplit Files per Environment and Agree on the Merge Order
Maintaining separate variable sets for local, test and production is common, but do not cram every environment’s values into a single .env — that effectively leaks production config to every developer. Safer is a combination of .env (common) + .env.local (local override only) + CI secrets (environment-specific).
The load order for the framework must be explicit. In the Nuxt/Vite ecosystem, the usual order is .env < .env.local < .env.<mode>. As long as the team agrees on this order and documents it in the README, and parsing stacks files in that sequence, you avoid arguments over which file wins.
Never store production secrets in a repo .env file; keep them in CI secrets management or the platform’s encrypted variables and inject at build time.
# 约定顺序(写进 README):.env 最弱,最后加载的覆盖
.env # 公共,所有环境
.env.local # 本地覆盖,不入库
.env.staging # 暂存环境(CI 注入)
# Nuxt CLI 会根据 mode 自动加载对应文件Verify: Variable Resolution, Missing-Value Checks and Secret Rotation
Run three layers of validation before launch. First, resolution: at startup, print each required variable’s name and whether it is present, confirming .env loaded and the current mode picked the right file.
Second, missing-value validation: on startup traverse the set of required variables and fail fast with a clear error if any is absent, instead of blowing up midway through execution. Third, security: scan code and repo for plaintext secrets and reconcile the source of each production value to confirm it comes from secrets storage.
Rotate secrets regularly, especially after departures or log leaks. Use a dual-write transition: let both old and new keys work, revoke the old one only after everyone switches, avoiding a single-shot replacement that breaks production.
# 缺失即失败的校验示例
const REQUIRED = ["DATABASE_URL", "API_KEY"];
for (const k of REQUIRED) {
if (!process.env[k]) throw new Error(`缺少必需的环境变量: ${k}`);
}
console.log("配置校验通过");