Working with Environment Variables

1. Installing dotenv Package

PackageUse Case
dotenvLoad .env in Node < 20.6
dotenv-expandVariable interpolation in .env
env-varType-safe parsing & validation
Native --env-file=.env NODE 20.6+No package needed

Example: Native env file

node --env-file=.env --env-file=.env.local app.js

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 });
OptionDefaultPurpose
path.envCustom file path or array
encodingutf8File encoding
overridefalseOverwrite existing env vars
processEnvprocess.envTarget object

3. Accessing Variables

PatternResult
process.env.PORTReturns string or undefined
+process.env.PORTCoerce 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
RuleDetail
No spaces around =KEY=value not KEY = value
Quote multi-word valuesK="hello world"
MultilineWrap in double quotes with \n

5. Using Different Environments

FileLoaded When
.envDefault (all environments)
.env.localLocal overrides (gitignored)
.env.developmentNODE_ENV=development
.env.productionNODE_ENV=production
.env.testNODE_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);
OperatorBehavior
||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
LibraryStrength
zodTypeScript-first, schema parsing
env-varLightweight, fluent API
envalidDesigned specifically for env
joiBattle-tested, verbose

8. Excluding .env from Git

Example: .gitignore

.env
.env.local
.env.*.local
# Track template / example
!.env.example
FileCommit?
.envNEVER (contains secrets)
.env.exampleYES (template with empty values)
.env.productionNEVER
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"])
};
MethodReturns
.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=productionDetail
Express view cacheTemplates compiled once
Generic error pagesStack traces hidden
npm installSkips devDependencies
Many librariesDisable debug logs, enable optimizations (React, etc.)

Example: Cross-platform env setting

# package.json scripts (cross-env for Windows compatibility)
"start": "cross-env NODE_ENV=production node app.js"
# Or natively in shell
NODE_ENV=production node app.js
# PM2
pm2 start app.js --env production