Working with Environment Variables
1. Installing dotenv Package
| Package | Use Case |
|---|---|
dotenv | Load .env in Node < 20.6 |
dotenv-expand | Variable interpolation in .env |
env-var | Type-safe parsing & validation |
Native --env-file=.env NODE 20.6+ | No package needed |
2. Loading Environment Variables
Example: dotenv import
// Must load BEFORE other imports that read process.env
import "dotenv/config";
import express from "express";
// Or with options
import dotenv from "dotenv";
dotenv.config({ path: ".env.production", override: false });
| Option | Default | Purpose |
|---|---|---|
path | .env | Custom file path or array |
encoding | utf8 | File encoding |
override | false | Overwrite existing env vars |
processEnv | process.env | Target object |
3. Accessing Variables
| Pattern | Result |
|---|---|
process.env.PORT | Returns string or undefined |
+process.env.PORT | Coerce to number (NaN if invalid) |
process.env.DEBUG === "true" | Manual boolean check |
JSON.parse(process.env.CONFIG) | Parse JSON env value |
4. Creating .env File
Example: .env
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://user:pass@localhost:5432/app
JWT_SECRET="multi word secret"
ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
# Comments allowed
LOG_LEVEL=info
| Rule | Detail |
|---|---|
No spaces around = | KEY=value not KEY = value |
| Quote multi-word values | K="hello world" |
| Multiline | Wrap in double quotes with \n |
5. Using Different Environments
| File | Loaded When |
|---|---|
.env | Default (all environments) |
.env.local | Local overrides (gitignored) |
.env.development | NODE_ENV=development |
.env.production | NODE_ENV=production |
.env.test | NODE_ENV=test (jest, vitest) |
6. Setting Default Values
Example: Defaults & coercion
const port = Number(process.env.PORT) || 3000;
const host = process.env.HOST ?? "0.0.0.0"; // ?? respects empty string
const debug = process.env.DEBUG === "true";
const origins = (process.env.ALLOWED_ORIGINS || "").split(",").filter(Boolean);
| Operator | Behavior |
|---|---|
|| | Falls back on falsy (incl. "0", "") |
?? | Falls back only on null/undefined |
7. Validating Required Variables
Example: Zod schema validation
import { z } from "zod";
const envSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32)
});
export const env = envSchema.parse(process.env); // throws on invalid
| Library | Strength |
|---|---|
zod | TypeScript-first, schema parsing |
env-var | Lightweight, fluent API |
envalid | Designed specifically for env |
joi | Battle-tested, verbose |
8. Excluding .env from Git
| File | Commit? |
|---|---|
.env | NEVER (contains secrets) |
.env.example | YES (template with empty values) |
.env.production | NEVER |
Warning: If a secret is ever committed, rotate it immediately — git history makes it permanent.
9. Using env-var for Validation
Example: env-var fluent API
import env from "env-var";
export const config = {
port: env.get("PORT").default(3000).asPortNumber(),
dbUrl: env.get("DATABASE_URL").required().asUrlString(),
origins: env.get("ALLOWED_ORIGINS").required().asArray(","),
logLevel: env.get("LOG_LEVEL").default("info").asEnum(["debug","info","warn","error"])
};
| Method | Returns |
|---|---|
.required() | Throws if missing |
.default(v) | Default value |
.asInt() / .asFloat() | Number with validation |
.asBool() / .asBoolStrict() | Boolean parsing |
.asArray(",") | Split by delimiter |
.asJson() | Parse JSON |
.asEnum([...]) | Whitelist |
10. Setting NODE_ENV to Production
| Effect of NODE_ENV=production | Detail |
|---|---|
| Express view cache | Templates compiled once |
| Generic error pages | Stack traces hidden |
| npm install | Skips devDependencies |
| Many libraries | Disable debug logs, enable optimizations (React, etc.) |