Environment & Config

Reviewed & published by Brayan K

Configuration is the set of values that change between machines — ports, database URLs, API keys — which you keep out of your code and read from environment variables on process.env.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll read environment variables with sensible defaults, load a .env file natively, convert string values to the right types, and centralize everything in a validated config module.

What You'll Learn in This Lesson

1️⃣ Reading process.env

Node puts every environment variable on the process.env object. Read a value, supply a default with ?? for when it's missing, and remember that every value is a string — convert with Number() when you need a number.

// Configuration that changes between machines — ports, secrets, database URLs —
// should NOT be hard-coded. Node exposes ENVIRONMENT VARIABLES on process.env.

// process.env is an object of strings provided by the operating system.
console.log("PORT from env:", process.env.PORT);     // -> undefined if not set

// Provide a sensible DEFAULT with the ?? operator (used when env is undefined).
const port = process.env.PORT ?? 3000;
console.log("Using port:", port);

// IMPORTANT: every value in process.env is a STRING. Convert when you need a number.
const workers = Number(process.env.WORKERS ?? "1");
console.log("Workers (a number):", workers, typeof workers);

// You set variables when you launch the program, e.g.:
//   PORT=8080 WORKERS=4 node app.js
// then process.env.PORT is "8080".

2️⃣ Loading a .env File

Typing variables on every launch gets old. A .env file holds KEY=value lines, and recent Node loads it natively with --env-file. Keep that file out of git so secrets stay private.

// Typing variables on every launch is tedious. The common pattern is a .env
// FILE holding key=value lines, loaded at startup.

// A file named ".env" in your project root might contain:
//   PORT=8080
//   DB_URL=sqlite://app.db
//   API_KEY=super-secret-123

// Node v20.6+ can load it natively with the --env-file flag (no package):
//   node --env-file=.env app.js

// After loading, the keys appear on process.env like any other variable.
// There is no .env file behind the Run button, so these two lines stand in
// for what --env-file would have put there. Remove them and the ?? defaults
// below take over, which is exactly what happens when the file is missing —
// worth trying, because that silent fallback is the usual cause of "why is
// it still on port 3000?".
process.env.PORT = "8080";
process.env.DB_URL = "sqlite://app.db";

const port = process.env.PORT ?? 3000;
const dbUrl = process.env.DB_URL ?? "sqlite://default.db";

console.log("Port:", port);
console.log("DB:", dbUrl);

// 🔒 Add .env to your .gitignore so secrets never get committed to git.

3️⃣ A Central, Validated Config Module

Reading process.env scattered across files gets messy. The clean pattern: a single config.js that reads, validates, and exports one object. Required values throw at startup if missing, so the app fails fast instead of breaking mysteriously later.

// Best practice: read env vars ONCE in a single config module, validate them,
// and export a clean object. The rest of your app imports config — not process.env.

// --- config.js ---
function required(name) {
  const value = process.env[name];
  // Fail FAST and LOUD if a required value is missing.
  if (value === undefined) {
    throw new Error(`Missing required env var: ${name}`);
  }
  return value;
}

// The Run button sets no environment at all, so give the required key a
// value first. Without it the very first thing this module does is throw —
// which is precisely what it is built to do, and what you want on a real
// server: a config error should stop the process at boot, not surface as a
// confusing 500 an hour later.
process.env.API_KEY = "super-secret-123";

const config = {
  port: Number(process.env.PORT ?? 3000),     // optional, with default
  env: process.env.NODE_ENV ?? "development",
  apiKey: required("API_KEY"),                 // required: throws if absent
};

module.exports = config;

// --- app.js ---
// const config = require("./config");
// console.log("Starting on port", config.port, "in", config.env, "mode");

console.log("port:", config.port, "env:", config.env);

// Never printed: the API key itself. Logging secrets is how they end up in
// a log aggregator that half the company can read.

// Now watch the loud failure. Ask for a variable that really is absent,
// caught here only so the rest of this block still runs — in a real app you
// would let it crash the process:
try {
  required("STRIPE_KEY");
} catch (err) {
  console.log("missing var ->", err.message);
}

Your turn. Fill in the two ___ blanks to read a host and port with defaults, then run it.

// 🎯 YOUR TURN — finish the config, then run: node app.js
// (try it with:  HOST=example.com node app.js  and also with no env set)

// TODO 1: read process.env.HOST, defaulting to "localhost" when it's missing.
const host = process.env.HOST ?? ___;        // 👉 the default host string

// TODO 2: read process.env.PORT, default 3000, and convert it to a NUMBER.
const port = ___(process.env.PORT ?? "3000"); // 👉 the function that makes a number

console.log(`Server would run at http://${host}:${port}`);

// ✅ With no env set:
//    Server would run at http://localhost:3000
// ✅ With HOST=example.com PORT=8080:
//    Server would run at http://example.com:8080

Mini-Challenge: A Validated Config Module

Build a real config.js with a required() helper, a number, a required string, and a boolean parsed from a string. Import it in app.js and print all three values.

// 🎯 MINI-CHALLENGE: a validated config module.
// Save as config.js (and a small app.js) and run: node --env-file=.env app.js
//
// 1. Write a helper required(name) that returns process.env[name],
//    or throws "Missing env var: NAME" when it's undefined.
// 2. Build a config object with:
//      port:   Number(process.env.PORT ?? 4000)
//      dbUrl:  required("DB_URL")
//      debug:  process.env.DEBUG === "true"   // string -> boolean
// 3. Export it with module.exports, import it in app.js, and print all three.
//
// ✅ With a .env of  DB_URL=sqlite://x.db  and  DEBUG=true :
//    port: 4000  dbUrl: sqlite://x.db  debug: true

// your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Config in Node

GoalCode
Read a variableprocess.env.PORT
Default if missingprocess.env.PORT ?? 3000
As a numberNumber(process.env.PORT)
As a booleanprocess.env.DEBUG === "true"
Set inlinePORT=8080 node app.js
Load a filenode --env-file=.env app.js

🎉 Lesson Complete!

Practice quiz

Where does Node expose environment variables?

  • process.argv
  • global.config
  • process.env
  • os.env

Answer: process.env. process.env is an object of environment variables provided by the OS or launch command.

What type is every value in process.env?

  • A string
  • A number
  • A boolean
  • It varies by key

Answer: A string. Environment variables come from the OS as text, so every process.env value is a string.

What does process.env.PORT ?? 3000 do?

  • Always uses 3000
  • Multiplies PORT by 3000
  • Throws if PORT is set
  • Uses 3000 only when PORT is undefined

Answer: Uses 3000 only when PORT is undefined. The ?? operator supplies the default 3000 only when process.env.PORT is null or undefined.

How do you turn process.env.WORKERS into a real number?

  • parseEnv(process.env.WORKERS)
  • Number(process.env.WORKERS)
  • process.env.WORKERS.toNumber()
  • It is already a number

Answer: Number(process.env.WORKERS). Use Number(...) to convert the string env value into a numeric type.

How can modern Node (v20.6+) load a .env file natively?

  • node --env-file=.env app.js
  • node --load-env app.js
  • require('dotenv') is required
  • node --config .env

Answer: node --env-file=.env app.js. Node v20.6+ reads a .env file with the --env-file flag, populating process.env before your code runs.

How do you set a variable inline for one launch?

  • node app.js PORT=8080
  • node --PORT 8080 app.js
  • PORT=8080 node app.js
  • export only in package.json

Answer: PORT=8080 node app.js. Prefixing the command, like PORT=8080 node app.js, sets the variable for that run.

What is the recommended way to handle config across your app?

  • Read process.env everywhere it is needed
  • Read env vars once in a config module and export a clean object
  • Hard-code values in each file
  • Store config in a global variable

Answer: Read env vars once in a config module and export a clean object. Best practice: read and validate env vars in one config module; the rest of the app imports config, not process.env.

How should a required env var behave when it is missing?

  • Silently default to an empty string
  • Retry until it appears
  • Log a warning and continue
  • Fail fast and loud by throwing an error at startup

Answer: Fail fast and loud by throwing an error at startup. Validate required values at startup and throw, so the app fails loudly instead of misbehaving later.

How do you convert a string env var to a boolean?

  • Boolean(process.env.DEBUG)
  • Compare it: process.env.DEBUG === 'true'
  • process.env.DEBUG.toBool()
  • It is already a boolean

Answer: Compare it: process.env.DEBUG === 'true'. Because env values are strings, the string 'false' is truthy; compare explicitly with === 'true'.

How do you keep secrets like API keys out of version control?

  • Commit them in the README
  • Encrypt the whole repo
  • Add .env to .gitignore and commit a .env.example instead
  • Put them in package.json

Answer: Add .env to .gitignore and commit a .env.example instead. Add .env to .gitignore so secrets are never pushed; commit a .env.example listing variable names with placeholders.

Continue this course

Frequently asked questions

What is process.env and why use it?

process.env is an object Node fills with the environment variables provided by your operating system or launch command. It's the standard place to keep configuration that differs between environments — the port to listen on, a database URL, an API key — without baking those values into your source code. That separation lets the exact same code run on your laptop, a teammate's machine, and a production server, each supplying its own values. It also keeps secrets out of the files you commit to version control.

Why is everything in process.env a string?

Environment variables come from the operating system, which only knows text — there's no concept of a number or boolean at that layer. So process.env.PORT is the string "3000", not the number 3000, and process.env.DEBUG is "true", not the boolean true. You must convert explicitly: Number(process.env.PORT) for numbers, and a comparison like process.env.DEBUG === "true" for booleans. Forgetting this leads to subtle bugs, such as string concatenation where you expected arithmetic.

Do I need the dotenv package to use a .env file?

Not anymore. Node.js version 20.6 and later can load a .env file natively with the --env-file flag: node --env-file=.env app.js reads the file and populates process.env before your code runs. On older Node versions, the popular dotenv package does the same thing — you call require("dotenv").config() at the very top of your entry file. Either way, a .env file is just a convenience so you don't have to type variables on the command line every time.

How do I keep secrets like API keys safe?

Never commit them. Put secrets in environment variables or a .env file, and add .env to your .gitignore so it's never pushed to a repository. Commit a .env.example file instead, listing the variable names with placeholder values, so teammates know what to set without seeing the real secrets. In production, set the variables through your hosting platform's configuration rather than a file. And validate required secrets at startup so the app fails loudly if one is missing, rather than misbehaving later.