Skip to main content
CodeOath
← All posts

Node.js95 min total · 14 parts

Node.js Fundamentals: The Runtime, the Event Loop, and Building Real APIs

Part 9 of 14 · ~3 min

Environment & Configuration

process.env

process.env is Node's window into the outside environment — a plain JavaScript object, every value a string, populated at startup from whatever launched the process in the first place:

const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;

if (!process.env.MERIDIAN_WEBHOOK_SECRET) {
  throw new Error("MERIDIAN_WEBHOOK_SECRET is required but was not set");
}

The whole point of pulling configuration from the environment instead of typing it into the source is that billhook.js itself never has to change between Ines's laptop, staging, and production — what changes is only what those variables resolve to: a local Postgres URL there, staging's here, Meridian's sandbox secret versus the live one, chatty logging versus quiet.

Why config should differ per environment

Development wants loud logs and a local database; production wants the opposite — minimal or structured logging, a connection pool sized for real traffic, and, critically, nothing sensitive ever hardcoded or checked into version control. NODE_ENV — by convention "development", "test", or "production" — is what a wide swath of the ecosystem, Express included, inspects to decide things like whether an error response gets a full stack trace or a sanitized one.

A real secret never belongs in a commit — not Meridian's live signing key, not the production database password, not even in a repository nobody outside the team can see. Keep the .env file holding actual values out of version control entirely via .gitignore, and commit a .env.example instead — same variable names, obviously fake values, so whoever sets this up next (including a future Ines or Devon who's forgotten the details) knows exactly what to fill in without that file ever having held anything real.

The .env bug that only showed up on Devon's machine

This one surprises almost everyone at least once: Node has no built-in awareness of .env files whatsoever. Whatever's sitting in process.env came from the actual shell or parent process that started Node running — a .env file in the project's root folder is just an ordinary text file as far as Node is concerned, until something explicitly reads and applies it. billhook uses the dotenv package for this:

require("dotenv").config(); // must run BEFORE anything that reads process.env
const dbUrl = process.env.DATABASE_URL; // now populated, assuming it's in .env

Sequencing is everything here: nothing that reads process.env.SOMETHING can run before dotenv.config() has actually populated it, which is exactly why that call conventionally opens the entry file and nothing else gets to run ahead of it. billhook hit exactly this bug when Devon split the Postgres connection setup into its own db.js and, without thinking about it, had server.js require("./routes/orders") — which itself required db.js — above the dotenv.config() line. db.js read process.env.DATABASE_URL at import time, before dotenv had run, and got undefined. It worked fine on Ines's machine, where DATABASE_URL happened to already be set as a real shell environment variable from an earlier project — so the bug was invisible to her and reproduced only on Devon's, purely because of which files got required in which order. (A native --env-file flag exists in newer Node releases as a dotenv-free alternative, though the underlying constraint doesn't go anywhere — whichever mechanism populates process.env, it still has to run before the code that depends on it does.)