Environment Variables
The same code runs on your laptop, in staging and in production — with different databases, keys and ports. Environment variables carry those differences, and keep secrets out of your source code.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Read configuration from process.env
- Load .env files with node --env-file or dotenv
- Validate and type configuration at startup
- Keep secrets out of Git and logs
Explanation#
process.env holds environment variables as strings: process.env.PORT is "3000", not 3000. Convert and validate.
For local development, put values in a .env file. Node 20.6+ can load it directly: node --env-file=.env server.js. The dotenv package does the same for older setups.
Commit a .env.example with every variable name and safe sample values; add .env to .gitignore.
Centralise configuration in one config.js module that reads, validates and exports a typed object. Fail fast at startup if something required is missing — better than a crash at 3 a.m. on the first payment.
In production, set variables through the platform (Docker, Kubernetes secrets, Azure App Service settings, AWS Secrets Manager), never in files committed to Git. Never log secrets.
Syntax#
# .env (never committed)
PORT=4000
DATABASE_URL=mysql://shop:secret@localhost:3306/shop
JWT_SECRET=change-me
node --env-file=.env server.js
Code example: A validated config module#
// In a real project, start with: node --env-file=.env server.js
// Here we set the variables in code so the example runs anywhere.
process.env.PORT = '4000';
process.env.NODE_ENV = 'production';
process.env.DATABASE_URL = 'mysql://shop:secret@db.internal:3306/shop';
process.env.CORS_ORIGINS = 'https://shop.example.com,https://admin.example.com';
delete process.env.JWT_SECRET; // pretend we forgot it
function loadConfig(env = process.env) {
const errors = [];
const required = name => {
const value = env[name];
if (!value) errors.push(`${name} is required`);
return value;
};
const config = {
port: Number(env.PORT ?? 3000),
isProduction: env.NODE_ENV === 'production',
databaseUrl: required('DATABASE_URL'),
jwtSecret: required('JWT_SECRET'),
corsOrigins: (env.CORS_ORIGINS ?? '').split(',').filter(Boolean),
};
if (!Number.isInteger(config.port)) errors.push('PORT must be a number');
if (errors.length) throw new Error('Invalid configuration:\n- ' + errors.join('\n- '));
return Object.freeze(config);
}
try {
loadConfig();
} catch (err) {
console.log(err.message);
}
process.env.JWT_SECRET = 'a-long-random-secret';
const config = loadConfig();
console.log('port:', config.port, typeof config.port);
console.log('production:', config.isProduction);
console.log('origins:', config.corsOrigins.length);
console.log('db host:', new URL(config.databaseUrl).hostname); // log the host, never the password
Invalid configuration: - JWT_SECRET is required port: 4000 number production: true origins: 2 db host: db.internal
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- All environment variables are strings; PORT is converted with Number.
- required() collects missing values instead of failing on the first one.
- The first call throws a clear message listing what is wrong.
- After setting JWT_SECRET, the config loads and is frozen so nobody can change it at runtime.
- Only the database host is logged — never the URL with its password.
Real-world use case#
The same Docker image of an Express API runs in staging and production. Kubernetes injects DATABASE_URL and JWT_SECRET from secrets; if one is missing, the pod fails at startup and the deployment is rolled back automatically.
Common mistakes#
Secrets in Git history stay there forever (and in every clone). Rotate any leaked secret and add .env to .gitignore.
"false" is truthy and "3000" + 1 is "30001". Convert and validate.
Scattered reads make it unclear what the app needs. Use one config module.
Best practices#
- One validated config module
- .env for local, platform secrets in production
- Commit .env.example, never .env
- Fail fast on missing configuration
Remember this#
process.env values are always strings.
node --env-file=.env loads a .env file (Node 20.6+).
Validate config at startup; never log secrets.
Practice exercise#
Your task: Write a function bool(name, defaultValue) that reads process.env[name] and returns true for "true"/"1", false for "false"/"0", the default if missing, and throws for anything else.
Need a hint?
Show solution
function bool(name, defaultValue) {
const raw = process.env[name];
if (raw === undefined) return defaultValue;
const v = raw.toLowerCase();
if (v === 'true' || v === '1') return true;
if (v === 'false' || v === '0') return false;
throw new Error(`${name} must be true/false, got "${raw}"`);
}
process.env.FEATURE_NEW_CHECKOUT = 'false';
console.log(bool('FEATURE_NEW_CHECKOUT', true)); // false
console.log(bool('FEATURE_MISSING', true)); // true
Mini quiz#
What type is process.env.PORT?
Want more? Practise all Node.js MCQs · Node.js interview questions
My notes#
Saved in this browser.