Introduction
Your code runs in more than one place: your laptop today, a classmate's machine tomorrow, a real server when the project goes live (the roadmap's 7.6 topic is this same idea from the hosting side). Each machine honestly differs — the free port, the database address, the JWT secret, whether it should behave as a careful live system or a chatty practice one. Beginners hard-code these values, then edit the code for every machine and eventually share a file with a real password inside it.
The professional pattern separates code from configuration. The code never changes; each machine keeps a small text file named .env holding its own settings, and the dotenv package loads them into process.env, where the code reads them like any variable. Secrets stop travelling with the code — and the same project runs everywhere by changing one little file, never the program. (Roman Urdu: Code sab ke liye aik, setting har machine ki apni — aur raaz hamesha .env mein, code mein kabhi nahi.)
Key points
.envis a plain settings file: oneNAME=valueper line —PORT=3000,MONGO_URL=...,JWT_SECRET=.... No quotes needed for simple values, no commas, no JavaScript — it is a list, not code.- dotenv loads it once, at the very top:
require("dotenv").config()reads.envand places every line intoprocess.env. It must run before any line that reads a setting, so it stands among the first lines of the server file. - Read settings with a fallback:
const PORT = process.env.PORT || 3000;— if the machine provides a port (real hosting always does), use it; otherwise the practice default serves. Lesson 7.4's hosting depends on exactly this line.(Roman Urdu: Machine jo de woh lo, warna apni pehchan wali setting —||yehi kaam karta hai.) - Everything in
process.envis text:process.env.PORTis the string"3000", not a number. Convert withNumber(...)when a real number is needed — the same text lesson as route parameters in N.3. - Development vs production is a setting too:
NODE_ENVis the community's agreed name —developmenton your laptop (chatty logs, open error details) andproductionlive (calm logs, hidden details — lesson N.9's behaviour switch). .envis never shared: it goes into.gitignoreon day one and is never uploaded, emailed or committed. What you do share is.env.example— the same file with the values emptied — so a teammate knows which settings exist without knowing any secret.- Missing settings should fail loudly at start: if
MONGO_URLis absent, crashing at startup with a clear message beats running a broken server that fails mysteriously per request.
Code examples
Example 1 — The .env file and reading it
PORT=3000
MONGO_URL=mongodb://localhost:27017/codescope
JWT_SECRET=meri-gizli-chaabi-786
NODE_ENV=development// server.js — the dotenv line goes at the very top
require("dotenv").config();
const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;
console.log("Mode:", process.env.NODE_ENV);
console.log("Database:", process.env.MONGO_URL);
app.get("/", (req, res) => {
res.send("Server in " + process.env.NODE_ENV + " mode");
});
app.listen(PORT, () => {
console.log("Server running on port " + PORT);
});What it does: Four settings live in .env; one config() line at the top loads them all. From then on the code reads process.env.NAME wherever a value is needed — the port for listen, the mode for messages, the database string for Mongoose (lesson N.6). Notice the code contains no secret and no machine-specific number at all: copy this folder to any computer with its own .env, and it runs correctly there without a single edit.
What you will see: the terminal prints the mode and database lines from your .env, the server starts on port 3000, and / answers Server in development mode. Change PORT to 5000 in .env, restart — the same untouched code now listens on 5000.
Example 2 — Fail loudly: settings the app cannot live without
require("dotenv").config();
const required = ["MONGO_URL", "JWT_SECRET"];
for (const name of required) {
if (!process.env[name]) {
console.log("Missing setting: " + name + " — add it to .env and restart");
process.exit(1);
}
}
console.log("All required settings present — starting the server");What it does: Before anything else starts, a small guard walks the list of settings the app truly cannot run without. A missing one is reported by name, with the remedy in the same line, and the process exits deliberately (process.exit(1) — the honest full stop). Compare the alternative: booting happily and then failing inside some poor user's request an hour later, with no hint that a setting was the cause.
What you will see: with both settings present, the "all present" line prints and startup continues; delete the JWT_SECRET line from .env and the server refuses to start, naming exactly what is missing.
Example 3 — Sharing the shape, not the secrets
PORT=
MONGO_URL=
JWT_SECRET=
NODE_ENV=.env
node_modulesWhat it does: The first block is .env.example — committed and shared proudly. It lists every setting's name with empty values: a new teammate copies it to .env and fills in their own machine's values. The second block is .gitignore — the two lines that keep the real .env (and the downloaded-packages folder from lesson N.1) out of every commit and upload. Together these two tiny files are the whole etiquette: the shape is public, the secrets are private.
What you will see: a fresh copy of the project made from the shared files contains .env.example and no .env — the project politely refuses to run until the new owner creates their own .env, exactly as Example 2 taught it to.
Quick recap
- Configuration lives outside code, in a per-machine
.envfile ofNAME=valuelines, loaded by dotenv intoprocess.env. - Read with fallbacks (
process.env.PORT || 3000); remember env values are always text. NODE_ENVseparates development behaviour from production behaviour..envis never shared or committed —.env.exampleshares the shape,.gitignoreenforces the privacy, and missing required settings fail loudly at startup.
Common mistakes
- Hard-coding a secret "just for testing" — the test file gets shared, committed or screenshotted, and the test password becomes a real breach. The fix: the first time a value differs per machine or must stay private, it goes into
.env— no temporary exceptions. - Loading dotenv too late — calling
config()after the lines that read the settings means those reads already happened and gotundefined. The fix:require("dotenv").config()is the first meaningful line of the entry file, before routes, models or connections. - Committing
.envonce and thinking deleting it later fixes it — in git, the old version stays in the history forever, password included. The fix:.enventers.gitignorebefore the first commit; if a secret ever did land in history, change the secret itself — it is compromised, not merely misplaced.
Practice
- Move the students API's port and a new setting,
SCHOOL_NAME, into.env. The home route should answerWelcome to <SCHOOL_NAME>. Run the server twice with two different.envvalues and confirm the code file never changed between runs. - Create
.env.exampleand.gitignorefor the project from Example 3, then deliberately removeMONGO_URLfrom your.envand start the server with the Example 2 guard in place. Write down the exact message — this is the message a teammate should meet on their first run.