Environment Variables
Read and manage runtime configuration through process.env and .env files
Overview
Key Points
- β’process.env is a plain object; all keys and values are strings (or undefined if not set)
- β’Always parse non-string values: parseInt(process.env.PORT, 10) and process.env.FLAG === 'true'
- β’dotenv reads a .env file and adds its entries to process.env at startup β install it as a dependency, not devDependency
- β’.env files must be in .gitignore; commit .env.example with placeholders instead
- β’Provide defaults with nullish coalescing: const port = process.env.PORT ?? 3000
- β’Validate required variables at startup and fail fast β a missing variable that causes an error hours later is harder to debug
Examples
Reading environment variables and providing defaults
// process.env returns undefined for unset variables
const port = parseInt(process.env.PORT ?? '3000', 10);
const nodeEnv = process.env.NODE_ENV ?? 'development';
const debug = process.env.DEBUG === 'true';
const apiKey = process.env.STRIPE_API_KEY; // undefined in development
console.log({ port, nodeEnv, debug, apiKey });Always provide a default so unset optional variables do not produce undefined mid-code.
Using dotenv for local development
// .env file (never commit this)
// PORT=4000
// DATABASE_URL=postgres://localhost:5432/mydb
// JWT_SECRET=supersecretkey
// At the top of your entry file (index.js / server.js):
require('dotenv').config(); // reads .env and populates process.env
// Or with ES modules:
// import 'dotenv/config';
const port = parseInt(process.env.PORT ?? '3000', 10);
const dbUrl = process.env.DATABASE_URL;
console.log('Port:', port); // 4000
console.log('DB URL:', dbUrl); // postgres://localhost:5432/mydbdotenv.config() is a no-op if the .env file is missing, so production (which sets vars through the platform) is unaffected.
Validating required environment variables at startup
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
// Call this once at startup
const config = {
port: parseInt(process.env.PORT ?? '3000', 10),
dbUrl: requireEnv('DATABASE_URL'),
jwtSecret: requireEnv('JWT_SECRET'),
nodeEnv: process.env.NODE_ENV ?? 'development',
};
// If DATABASE_URL is missing, the process exits immediately with a clear error
// rather than crashing with a cryptic "undefined is not a string" later.
module.exports = config;Setting environment variables when launching Node
# Set inline (Unix/macOS/Linux)
PORT=8080 NODE_ENV=production node server.js
# Set with cross-env for Windows compatibility (npm package)
# npx cross-env PORT=8080 NODE_ENV=production node server.js
# Read back inside the process:
console.log(process.env.PORT); // "8080" (string, not number)
console.log(process.env.NODE_ENV); // "production"cross-env is a popular package that sets env vars in a cross-platform way for npm scripts.
Common Mistakes
- Treating process.env values as numbers or booleans β they are always strings; parseInt("3000", 10) not just +"3000"
- Committing .env files to git β use .gitignore and .env.example to document required variables without exposing secrets
- Not validating required variables at startup β missing variables fail silently until the code path that uses them is hit
- Calling dotenv.config() after process.env is read β dotenv must be called before any code that reads environment variables
Note
Structure a dedicated config.ts or config.js file that reads all environment variables in one place, parses/validates them, and exports a typed object. The rest of your app imports config, not process.env directly β this makes the env contract explicit and keeps all parsing logic in one place.
Exercises
Write Node.js code in your local terminal. Use the editor below to follow along, then mark each exercise as done once it works.
Completed: 0 / 6 exercises
Knowledge Check
Question 1 of 4