🌱BeginnerNode.js Foundations

Environment Variables

Read and manage runtime configuration through process.env and .env files

Overview

Environment variables are the standard mechanism for injecting runtime configuration into a Node.js process without hard-coding values in your source code. Database credentials, API keys, port numbers, and feature flags should never live in your codebase β€” they belong in environment variables that change per deployment (development, staging, production) without requiring code changes or redeployment. In Node.js, all environment variables are accessible through the `process.env` object. Every key is a string, and every value is either a string or `undefined` β€” there are no numbers or booleans in the environment. This means you must parse or convert values when you need a different type: `parseInt(process.env.PORT)` for numbers, and `process.env.DEBUG === 'true'` for booleans. For local development, the community convention is to use a `.env` file at the project root. The `dotenv` package reads this file and populates `process.env` at startup. The `.env` file is always listed in `.gitignore` β€” it is a local secret store, never committed to version control. A `.env.example` file with placeholder values documents which variables the project needs without exposing real credentials. In production environments, variables are set through the deployment platform (Heroku config vars, Railway variables, Kubernetes Secrets, Docker --env-file, AWS Parameter Store, etc.). Your code reads from `process.env` the same way regardless of how the variable was set β€” the source is transparent to your application.

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/mydb

dotenv.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.

Loading editor…

Completed: 0 / 6 exercises

Knowledge Check

Question 1 of 4

What type does process.env.PORT return when PORT is set to "3000" in the shell?