Stop Hardcoding Secrets
We've all been there: a config file with API_KEY = "sk-123456" sitting in the repo. It works until it doesn't. A teammate pushes it to GitHub, a bot scrapes it, and now your credit card bill is someone else's shopping spree.
Environment variables are the standard fix, but doing it wrong is almost as bad as not doing it at all. Here's how to handle them safely in real projects.
The Basics: What Are Env Vars?
Environment variables are key-value pairs available to your process at runtime. They keep secrets out of code and let you change behavior without touching source files.
export DATABASE_URL="postgres://user:pass@localhost:5432/mydb"
In Node.js, you read them with process.env.DATABASE_URL. In Python, os.environ.get("DATABASE_URL"). Simple enough.
The .env File Trap
Tools like dotenv load variables from a .env file. That's convenient for local dev, but it's a trap if you're not careful.
The rule: never commit .env. Add it to .gitignore immediately.
# .gitignore
.env
But here's the catch: if you use .env, you need a template. Commit .env.example with dummy values so other developers know what to set.
# .env.example
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
API_KEY=changeme
PORT=3000
Now teammates can copy it to .env and fill in real values.
Don't Use One File for Everything
Separate concerns. Local development, staging, and production have different needs. Don't force a single .env to handle all cases.
For local dev, dotenv is fine. For production, use your hosting platform's secret manager (AWS Secrets Manager, Vercel Env Variables, etc.). Never ship a .env file to a server.
Validate Early, Fail Fast
Missing env vars are a common source of bugs. Instead of crashing deep in your code with a cryptic error, validate at startup.
const required = ["DATABASE_URL", "API_KEY", "PORT"];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required env var: ${key}`);
}
}
Or use a library like envalid in Node or pydantic settings in Python. They give you type coercion and clear error messages.
Defaults with Caution
Defaults are handy for non-secret values like PORT=3000. But never default secrets. An empty string default for an API key is still a secret that's missing.
const port = process.env.PORT || 3000; // fine
const apiKey = process.env.API_KEY; // no default!
Avoid Committing .env Files in CI
If your CI needs env vars, set them in the CI service's settings, not in the repo. GitHub Actions has secrets, GitLab has variables, and they're all encrypted. Use those.
Watch Out for Logging
Don't log env vars. It's tempting to debug with console.log(process.env), but that can leak secrets into logs. If you must log, redact values.
function safeEnv() {
const copy = { ...process.env };
for (const key of Object.keys(copy)) {
if (/KEY|SECRET|PASSWORD|TOKEN/i.test(key)) {
copy[key] = "[REDACTED]";
}
}
return copy;
}
Use a Library for Complex Config
For larger projects, manage config with a library that supports nested structures and validation. In Node, config or convict are good. In Python, pydantic is excellent.
from pydantic import BaseSettings
class Settings(BaseSettings):
database_url: str
api_key: str
port: int = 3000
class Config:
env_file = ".env"
settings = Settings()
Now you get automatic validation and type conversion.
Rotate Secrets Regularly
Even with perfect practices, secrets leak. Rotate them periodically. Use your cloud provider's rotation features or set reminders.
The Golden Rules
-
Never commit real secrets. Use
.env.exampleand.gitignore. - Load env vars at process start. Don't fetch them lazily in random places.
- Validate everything. Fail fast with clear messages.
- Use platform secret managers in production.
- Redact logs.
Environment variables are a simple tool, but they require discipline. Follow these practices and you'll sleep better knowing your secrets are safe.
Top comments (1)
the startup validation and separate environments are the most useful rules here. i would add a secret scan in pre commit and ci, plus a boot check that reports the variable name and expected type without printing the value. for rotation, support two active keys during a short overlap, then revoke the old one after all instances reload. that reduces failed deploys while keeping the secret out of the repo.