Node.js is the “N” in MERN and the runtime that makes everything else in the stack possible on the server. Before Node.js existed, JavaScript was a browser-only language. Node.js brought JavaScript to the server in 2009 by embedding Chrome’s V8 JavaScript engine into a standalone runtime. This was the breakthrough that made full-stack JavaScript development — and therefore the MERN stack — possible. In this lesson you will understand what Node.js is, how its event loop and non-blocking I/O model work, what npm provides, and exactly how Node.js acts as the foundation for your Express API.
Node.js in the MERN Stack
| Layer | Technology | Depends on Node.js? | How |
|---|---|---|---|
| Database | MongoDB | No — standalone server | Node connects to it via the MongoDB driver |
| API Server | Express.js | Yes — Express is a Node.js framework | Express runs inside Node’s HTTP module |
| Frontend (dev) | React / Vite | Yes — dev server and build tools | Vite runs on Node; built output is browser JS |
| Package manager | npm | Yes — bundled with Node | npm installs all packages for all layers |
node index.js, Node.js reads your JavaScript file, compiles it with the V8 engine, and executes it. The key difference from the browser is that Node.js has access to the file system, operating system, network sockets, and environment variables — but has no window, document, or browser DOM APIs. Server-side and client-side JavaScript share the same syntax but have completely different global environments..nvmrc file.The Event Loop — Non-Blocking I/O
The most important concept to understand about Node.js is its non-blocking I/O model. Traditional servers (like Apache with PHP) spawn a new thread for every incoming request. Node.js uses a single thread and an event loop that handles many concurrent requests by never waiting.
Traditional (blocking) server — one thread per request:
Request 1 → Thread 1 → wait for DB (50ms) → respond → Thread 1 freed
Request 2 → Thread 2 → wait for DB (50ms) → respond → Thread 2 freed
Request 3 → Thread 3 → wait for DB (50ms) → respond → Thread 3 freed
1000 requests → 1000 threads → high memory, context-switching overhead
Node.js (non-blocking) — single thread + event loop:
Request 1 → register DB callback → continue ──────────────┐
Request 2 → register DB callback → continue ────────────┐ │
Request 3 → register DB callback → continue ──────────┐ │ │
│ │ │
DB results arrive → event loop picks up callbacks ←───┘ ┘ ┘
All three requests handled with one thread — minimal memory
Asynchronous Patterns in Node.js
// ── 1. Callbacks (old style — still seen in legacy code) ─────────────────
const fs = require('fs');
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) return console.error(err);
console.log(data); // runs when file is ready — not blocking
});
console.log('This runs BEFORE the file is read'); // proves non-blocking
// ── 2. Promises (modern) ─────────────────────────────────────────────────
const fs = require('fs/promises');
fs.readFile('data.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));
// ── 3. async/await (most readable — used throughout MERN) ────────────────
async function readConfig() {
try {
const data = await fs.readFile('data.txt', 'utf8'); // waits, but does NOT block
return JSON.parse(data);
} catch (err) {
console.error('Failed to read config:', err.message);
}
}
Node.js Core Modules Used in MERN
| Module | Require | Used For |
|---|---|---|
| http / https | require('http') |
Express is built on top of this — rarely used directly |
| fs / fs/promises | require('fs/promises') |
Reading config files, writing logs, handling uploaded files |
| path | require('path') |
Building file paths that work on Windows and Unix |
| os | require('os') |
Reading CPU count, memory, temp directory |
| crypto | require('crypto') |
Generating tokens, hashing — used in auth flows |
| process | Global — no require | Reading environment variables via process.env |
npm — Node Package Manager
# ── Core npm commands you will use daily in MERN development ─────────────
npm init -y # create package.json with defaults
npm install express mongoose # install production dependencies
npm install -D nodemon # install dev-only dependency
npm install # install all dependencies from package.json
npm run dev # run the "dev" script from package.json
npm run start # run the "start" script (production)
npm list # list installed packages
npm outdated # check for outdated packages
npm update # update packages within semver range
npm audit # check for security vulnerabilities
npm audit fix # auto-fix safe vulnerabilities
Environment Variables with process.env
// server/.env (never commit this file to Git)
PORT=5000
MONGODB_URI=mongodb://localhost:27017/blogdb
JWT_SECRET=super_secret_key_change_in_production
NODE_ENV=development
EMAIL_USER=you@gmail.com
EMAIL_PASS=yourapppassword
// server/index.js — load .env before anything else
require('dotenv').config(); // reads .env and populates process.env
const PORT = process.env.PORT || 5000;
const MONGODB_URI = process.env.MONGODB_URI;
const JWT_SECRET = process.env.JWT_SECRET;
console.log(`Starting in ${process.env.NODE_ENV} mode`);
Common Mistakes
Mistake 1 — Using require and ES Modules import together
❌ Wrong — mixing CommonJS and ES Module syntax in the same project without configuration:
import express from 'express'; // ES Module syntax
const mongoose = require('mongoose'); // CommonJS syntax — cannot mix without setup
✅ Correct — pick one system. For Node.js Express projects, CommonJS (require) is the default and requires no configuration. To use import either set "type": "module" in package.json or use .mjs file extensions.
Mistake 2 — Storing secrets in code instead of environment variables
❌ Wrong — hardcoding secrets directly in source files that get committed to Git:
const JWT_SECRET = 'mysecretkey123'; // visible to everyone who sees the repo
mongoose.connect('mongodb+srv://admin:password@cluster.mongodb.net/db');
✅ Correct — always use process.env with a .env file that is listed in .gitignore:
const JWT_SECRET = process.env.JWT_SECRET;
mongoose.connect(process.env.MONGODB_URI);
Mistake 3 — Not handling promise rejections
❌ Wrong — async operations without error handling silently fail or crash the process:
app.get('/api/posts', async (req, res) => {
const posts = await Post.find(); // if MongoDB is down — unhandled rejection crash
res.json(posts);
});
✅ Correct — always wrap async route handlers in try/catch:
app.get('/api/posts', async (req, res, next) => {
try {
const posts = await Post.find();
res.json({ success: true, data: posts });
} catch (err) {
next(err); // passes to global error middleware
}
});
Quick Reference
| Task | Command / Code |
|---|---|
| Check Node version | node --version |
| Run a JS file | node index.js |
| Read env variable | process.env.PORT |
| Load .env file | require('dotenv').config() |
| Import built-in module | const path = require('path') |
| Install package | npm install package-name |
| Install dev dependency | npm install -D package-name |
| Run npm script | npm run script-name |
| Auto-restart on change | npx nodemon index.js |