Stop Hardcoding, Start Centralizing
We've all been there: const apiUrl = 'https://staging.example.com' sitting at the top of a random file. Then someone pushes it to production and the app starts talking to staging. Or worse, you have ten .env files scattered across microservices with slightly different variable names. This is a config nightmare.
The clean way is to treat environment configuration as a first-class concern: centralize it, validate it, and access it through a typed, consistent interface. Here's how I do it in Node.js, and the pattern translates to any language.
The Core Principle: One Place, One Truth
All environment variables should be loaded in a single module, transformed into a structured object, and exported. No other file should read process.env directly. This gives you a single place to add defaults, validate types, and document what's available.
Step 1: Load and Parse
First, I use dotenv to load a .env file in development (never commit it, but do commit a .env.example). Then I read variables with sensible defaults.
// config/index.js
import dotenv from 'dotenv';
dotenv.config();
const config = {
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT, 10) || 3000,
database: {
url: process.env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE, 10) || 10,
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
},
logLevel: process.env.LOG_LEVEL || 'info',
};
export default config;
Note the parseInt calls. Environment variables are always strings, so converting to numbers here prevents bugs later. Also, I'm grouping related settings into nested objects, which keeps the export clean.
Step 2: Validate Early, Fail Fast
Missing required variables should crash the app at startup, not halfway through a request. I use a tiny validation function or a library like envalid. Here's a manual check that's easy to read:
// config/validate.js
function required(value, name) {
if (value === undefined || value === '') {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
const config = {
// ...
database: {
url: required(process.env.DATABASE_URL, 'DATABASE_URL'),
// ...
},
};
If you want a more declarative approach, envalid is great. It gives you types, defaults, and validation in one shot.
Step 3: Use a Config Service in Your App
Now, instead of importing process.env everywhere, you import your config object. For example, in an Express app:
// app.js
import config from './config/index.js';
import express from 'express';
const app = express();
app.listen(config.port, () => {
console.log(`Server running in ${config.env} on port ${config.port}`);
});
In a database client:
// db.js
import config from './config/index.js';
const pool = new Pool({ connectionString: config.database.url });
This makes your code testable too. In tests, you can override config with a mock object, no need to mess with process.env.
Step 4: Keep Secrets Out of Code
Never hardcode secrets like API keys or passwords. Use environment variables, and for production, use a secrets manager (AWS Secrets Manager, Vault, etc.) that injects them into the environment at runtime. Your config module doesn't care where they come from; it just reads them.
Step 5: Document with a .env.example
Commit a .env.example file so other developers know what to set. Include comments for each variable.
# .env.example
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
DB_POOL_SIZE=10
REDIS_HOST=localhost
REDIS_PORT=6379
LOG_LEVEL=info
What About Other Languages?
The same pattern applies everywhere. In Python, use pydantic or python-dotenv. In Go, use envconfig or viper. The idea is universal: centralize, validate, and expose a typed config object.
The Payoff
- No more magic strings scattered across the codebase.
- Startup fails fast if required config is missing.
- Easy to test by mocking the config module.
-
Onboarding is simpler with a clear
.env.example.
It's a small investment that pays off every time you deploy to a new environment or debug a configuration issue. Your future self will thank you.
Further Reading
- Node.js dotenv documentation
- MDN: Environment variables (general concept)
Happy configuring!
Top comments (1)
Use varlock - free and open source. Works with everything. You don’t need to reinvent the wheel.