Introduction to Node.js
Beginner · 5 min read
Node.js is a runtime environment that lets you run JavaScript on the server side, built on Chrome's V8 engine. It uses an event-driven, non-blocking I/O model that makes it lightweight and efficient — ideal for real-time applications and APIs.
Why Node.js?
- JavaScript everywhere — same language on frontend & backend
- Non-blocking I/O — handles thousands of concurrent connections
- npm ecosystem — 2 million+ packages
- Great for REST APIs, microservices, real-time apps, CLIs
- Used by Netflix, LinkedIn, Uber, PayPal
Node.js Event Loop
Node.js is single-threaded but handles concurrency via the event loop. When an async operation (file I/O, DB query, HTTP call) starts, Node delegates it to the OS/thread pool and continues processing other requests. When the operation completes, its callback is queued for execution.
💡 Node.js is not suitable for CPU-heavy operations (image processing, ML inference) — those block the event loop. Use worker threads or offload to a separate service.
Setup & npm
Beginner · 7 min read
# Install Node.js via https://nodejs.org (LTS recommended)
node --version # v20.x or v22.x
npm --version # 10.x
# Initialize a project
mkdir my-api && cd my-api
npm init -y # creates package.json with defaults
# Install packages
npm install express mongoose dotenv bcrypt jsonwebtoken
npm install --save-dev nodemon jest supertest
# package.json scripts
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest"
}
}
# Run
npm run dev # auto-restart on file changes
# Useful npm commands
npm list # list installed packages
npm outdated # show outdated packages
npm update # update within semver range
npm audit # check for vulnerabilities
npm audit fix # auto-fix vulnerabilities
npx # run package without installing globally
Module System (CJS / ESM)
Beginner · 8 min read
// CommonJS (CJS) — default in Node.js (.js files)
// math.js
function add(a, b) { return a + b; }
const PI = 3.14159;
module.exports = { add, PI };
// OR: exports.add = add;
// main.js — require
const { add, PI } = require('./math'); // local module
const fs = require('fs'); // built-in
const express = require('express'); // npm package
// ES Modules (ESM) — add "type":"module" in package.json
// math.mjs or with "type":"module"
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export default class Calculator { ... }
// import
import { add, PI } from './math.js';
import Calculator from './math.js';
import * as Math from './math.js';
// Dynamic import (works in both CJS and ESM)
const { add } = await import('./math.js');
// __dirname and __filename (ESM — not available by default)
import { fileURLToPath } from 'url';
import path from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
File System (fs)
Beginner · 8 min read
const fs = require('fs');
const fsp = require('fs/promises'); // Promise-based (preferred)
const path = require('path');
// Async read (callback)
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// Promise-based (async/await)
const data = await fsp.readFile('data.txt', 'utf8');
await fsp.writeFile('out.txt', 'Hello\n');
await fsp.appendFile('log.txt', 'New entry\n');
await fsp.unlink('temp.txt'); // delete file
await fsp.mkdir('uploads', { recursive: true });
const files = await fsp.readdir('.');
// Sync (avoid in server code — blocks event loop)
const config = JSON.parse(fs.readFileSync('config.json', 'utf8'));
// Watch for changes
fs.watch('./src', { recursive: true }, (event, filename) => {
console.log(`${event}: ${filename}`);
});
// path utilities
path.join(__dirname, 'uploads', 'avatar.jpg') // OS-safe join
path.resolve('./config.json') // absolute path
path.extname('file.jpg') // '.jpg'
path.basename('path/file.js') // 'file.js'
path.dirname('path/file.js') // 'path'
path & os Modules
Beginner · 5 min read
const os = require('os');
const path = require('path');
// os — system information
os.platform() // 'linux', 'darwin', 'win32'
os.arch() // 'x64', 'arm64'
os.cpus() // array of CPU info
os.totalmem() // total RAM in bytes
os.freemem() // free RAM in bytes
os.homedir() // '/Users/aftab'
os.tmpdir() // temp directory
os.hostname() // machine name
os.uptime() // seconds since boot
os.EOL // '\n' (Linux/Mac) or '\r\n' (Windows)
// Useful derived values
const cpuCount = os.cpus().length; // for clustering
const ramGB = os.totalmem() / 1024 ** 3;
// path — cross-platform file paths
path.sep // '/' on Linux/Mac, '\' on Windows
path.delimiter // ':' on Linux/Mac, ';' on Windows (for PATH)
path.parse('/home/user/file.txt')
// { root:'/', dir:'/home/user', base:'file.txt', ext:'.txt', name:'file' }
path.format({ dir: '/home', name: 'file', ext: '.txt' })
// '/home/file.txt'
EventEmitter
Intermediate · 8 min read
Node.js is event-driven. EventEmitter is the core class that allows objects to emit named events and attach listener functions.
const { EventEmitter } = require('events');
class OrderService extends EventEmitter {
placeOrder(order) {
// ... process order ...
this.emit('order:placed', order);
if (order.isPriority) this.emit('order:priority', order);
}
}
const svc = new OrderService();
// on — persistent listener
svc.on('order:placed', (order) => {
console.log('New order:', order.id);
sendEmail(order.customerEmail);
});
// once — listener fires only once
svc.once('order:priority', (order) => {
alertAdmin(order);
});
// off / removeListener
const logFn = (o) => console.log(o);
svc.on('order:placed', logFn);
svc.off('order:placed', logFn);
// error event (must always be handled!)
svc.on('error', (err) => console.error('OrderService error:', err));
// Unhandled 'error' events crash the process
svc.placeOrder({ id: 'ORD-001', customerEmail: 'a@b.com' });
Streams & Buffers
Advanced · 10 min read
Streams process data chunk-by-chunk instead of loading it all into memory. Essential for large files, video streaming, and pipe-based data pipelines.
const fs = require('fs');
const { Transform, pipeline } = require('stream');
const { promisify } = require('util');
const pipe = promisify(pipeline);
// Readable stream — read large file without loading all in RAM
const readable = fs.createReadStream('huge-file.csv', { encoding: 'utf8' });
readable.on('data', chunk => console.log('Chunk:', chunk.length));
readable.on('end', () => console.log('Done'));
readable.on('error', err => console.error(err));
// Writable stream
const writable = fs.createWriteStream('output.txt');
writable.write('line 1\n');
writable.end();
// Pipe (read → transform → write) — memory efficient
const zlib = require('zlib');
await pipe(
fs.createReadStream('input.txt'),
zlib.createGzip(), // transform: compress
fs.createWriteStream('input.txt.gz')
);
// Custom Transform stream
const upperCase = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
});
// Buffer — raw binary data
const buf = Buffer.from('Hello', 'utf8');
buf.toString(); // 'Hello'
buf.toString('hex'); // '48656c6c6f'
buf.toString('base64'); // 'SGVsbG8='
Buffer.alloc(16); // 16 zero bytes
Buffer.concat([buf1, buf2]);
HTTP Module
Beginner · 7 min read
const http = require('http');
// Create a basic HTTP server
const server = http.createServer((req, res) => {
const { method, url, headers } = req;
// Parse body (manual)
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const parsed = body ? JSON.parse(body) : {};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ method, url, body: parsed }));
});
});
server.listen(3000, () => console.log('Server on http://localhost:3000'));
// HTTP client (make outgoing requests)
const https = require('https');
https.get('https://api.github.com/users/torvalds', {
headers: { 'User-Agent': 'my-app' }
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => console.log(JSON.parse(data)));
});
💡 In real projects you'll use Express.js instead of the raw http module — it handles routing, middleware, and parsing for you.
Express.js Setup
Beginner · 8 min read
npm install express
// src/index.js
const express = require('express');
const app = express();
// Built-in middleware
app.use(express.json()); // parse JSON body
app.use(express.urlencoded({ extended: true })); // parse form data
app.use(express.static('public')); // serve static files
// Routes
app.get('/', (req, res) => {
res.json({ message: 'Hello from Express!' });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Project Structure
src/
├── index.js # entry: setup app, connect DB, start server
├── app.js # express app setup (middleware, routes)
├── routes/
│ ├── users.js
│ └── products.js
├── controllers/
│ ├── userController.js
│ └── productController.js
├── models/
│ └── User.js
├── middleware/
│ ├── auth.js
│ └── errorHandler.js
└── config/
└── db.js
Routing
Beginner · 8 min read
// routes/users.js
const router = require('express').Router();
const { getUsers, getUser, createUser, updateUser, deleteUser }
= require('../controllers/userController');
// RESTful routes
router.get('/', getUsers); // GET /users
router.get('/:id', getUser); // GET /users/42
router.post('/', createUser); // POST /users
router.put('/:id', updateUser); // PUT /users/42
router.delete('/:id', deleteUser); // DELETE /users/42
module.exports = router;
// app.js — mount router
app.use('/api/users', require('./routes/users'));
// Route parameters & query strings
router.get('/:id/orders', (req, res) => {
const { id } = req.params; // /users/42/orders
const { page, limit } = req.query; // ?page=2&limit=10
res.json({ id, page, limit });
});
// chain routes on same path
router.route('/:id')
.get(getUser)
.put(updateUser)
.delete(deleteUser);
Middleware
Intermediate · 10 min read
Middleware functions have access to req, res, and next. They run in order and can modify the request, send a response, or pass control to the next middleware.
// Logger middleware
function logger(req, res, next) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // MUST call next() or request hangs
}
app.use(logger);
// Rate limiter
const rateLimit = require('express-rate-limit');
app.use('/api', rateLimit({
windowMs: 15 * 60 * 1000, // 15 min
max: 100, // 100 requests per window
message: 'Too many requests'
}));
// CORS
const cors = require('cors');
app.use(cors({
origin: ['https://myapp.com', 'http://localhost:5173'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true
}));
// Helmet — security headers
const helmet = require('helmet');
app.use(helmet());
// Route-specific middleware
function requireAdmin(req, res, next) {
if (req.user?.role !== 'admin')
return res.status(403).json({ error: 'Forbidden' });
next();
}
router.delete('/:id', requireAdmin, deleteUser);
Building a REST API
Intermediate · 12 min read
// controllers/userController.js
const User = require('../models/User');
const getUsers = async (req, res, next) => {
try {
const { page = 1, limit = 10, sort = '-createdAt' } = req.query;
const users = await User
.find()
.sort(sort)
.limit(limit * 1)
.skip((page - 1) * limit)
.select('-password'); // exclude password
const total = await User.countDocuments();
res.json({ users, total, page, pages: Math.ceil(total / limit) });
} catch (err) { next(err); }
};
const createUser = async (req, res, next) => {
try {
const { name, email, password } = req.body;
if (!name || !email || !password)
return res.status(400).json({ error: 'All fields required' });
const exists = await User.findOne({ email });
if (exists) return res.status(409).json({ error: 'Email exists' });
const hashed = await bcrypt.hash(password, 12);
const user = await User.create({ name, email, password: hashed });
res.status(201).json({ id: user._id, name: user.name, email: user.email });
} catch (err) { next(err); }
};
module.exports = { getUsers, createUser };
Error Handling
Intermediate · 8 min read
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
console.error(err.stack);
const status = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
// Mongoose validation error
if (err.name === 'ValidationError') {
return res.status(400).json({
error: Object.values(err.errors).map(e => e.message)
});
}
// Mongoose duplicate key
if (err.code === 11000) {
return res.status(409).json({ error: 'Duplicate value' });
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
}
res.status(status).json({ error: message });
}
// app.js — register LAST
app.use(errorHandler);
// 404 handler
app.use((req, res) => res.status(404).json({ error: 'Route not found' }));
// Async wrapper to avoid try/catch everywhere
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
router.get('/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
}));
MongoDB with Mongoose
Intermediate · 12 min read
npm install mongoose
// config/db.js
const mongoose = require('mongoose');
module.exports = async () => {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('MongoDB connected');
};
// models/User.js
const { Schema, model } = require('mongoose');
const userSchema = new Schema({
name: { type: String, required: true, trim: true, maxlength: 50 },
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true, select: false },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
avatar: String,
active: { type: Boolean, default: true }
}, {
timestamps: true // adds createdAt, updatedAt
});
// Virtual field
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
// Pre-save middleware
userSchema.pre('save', async function(next) {
if (this.isModified('password'))
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Instance method
userSchema.methods.comparePassword = async function(candidate) {
return bcrypt.compare(candidate, this.password);
};
module.exports = model('User', userSchema);
JWT Authentication
Intermediate · 12 min read
npm install jsonwebtoken bcryptjs
// auth controller
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const login = async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !await bcrypt.compare(password, user.password))
return res.status(401).json({ error: 'Invalid credentials' });
const token = jwt.sign(
{ id: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({ token, user: { id: user._id, name: user.name } });
};
// middleware/auth.js
const protect = async (req, res, next) => {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer '))
return res.status(401).json({ error: 'No token' });
try {
const decoded = jwt.verify(auth.split(' ')[1], process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch (err) {
res.status(401).json({ error: 'Token invalid or expired' });
}
};
// Usage: protect route
router.get('/profile', protect, getProfile);
File Uploads (Multer)
Intermediate · 8 min read
npm install multer
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
// Storage config — save to disk
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, 'uploads/'),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const unique = crypto.randomUUID();
cb(null, `${unique}${ext}`);
}
});
// File filter — whitelist allowed types
const fileFilter = (req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
if (allowed.includes(file.mimetype)) cb(null, true);
else cb(new Error('Only images allowed'), false);
};
const upload = multer({
storage,
fileFilter,
limits: { fileSize: 5 * 1024 * 1024 } // 5MB
});
// Single file
router.post('/avatar', protect, upload.single('avatar'), async (req, res) => {
const url = `/uploads/${req.file.filename}`;
await User.findByIdAndUpdate(req.user.id, { avatar: url });
res.json({ url });
});
// Multiple files
router.post('/gallery', upload.array('images', 10), (req, res) => {
const urls = req.files.map(f => `/uploads/${f.filename}`);
res.json({ urls });
});
Environment Variables
Intermediate · 7 min read
npm install dotenv
# .env (NEVER commit this to git)
NODE_ENV=development
PORT=3000
MONGO_URI=mongodb://localhost:27017/myapp
JWT_SECRET=super_secret_key_change_in_prod
REDIS_URL=redis://localhost:6379
CORS_ORIGIN=http://localhost:5173
// Load at very top of index.js
require('dotenv').config();
// config/index.js — centralize config
module.exports = {
port: process.env.PORT || 3000,
mongoUri: process.env.MONGO_URI,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV || 'development',
isDev: process.env.NODE_ENV !== 'production',
corsOrigin: process.env.CORS_ORIGIN?.split(',')
};
# .env.example (commit this as documentation)
NODE_ENV=development
PORT=3000
MONGO_URI=mongodb://localhost:27017/myapp
JWT_SECRET=your_jwt_secret_here
# .gitignore — always ignore .env files
.env
.env.local
.env.production
WebSockets (Socket.io)
Advanced · 10 min read
npm install socket.io
// server: src/index.js
const http = require('http');
const { Server } = require('socket.io');
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: process.env.CORS_ORIGIN }
});
// Middleware: authenticate socket connections
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
try {
socket.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (e) { next(new Error('Authentication error')); }
});
io.on('connection', (socket) => {
console.log(`User connected: ${socket.user.id}`);
socket.join(`user:${socket.user.id}`); // private room
socket.on('message:send', ({ roomId, text }) => {
io.to(roomId).emit('message:new', {
text, sender: socket.user.id, time: new Date()
});
});
socket.on('disconnect', () => console.log('User left'));
});
// Client-side
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000', {
auth: { token: localStorage.getItem('token') }
});
socket.emit('message:send', { roomId: 'room1', text: 'Hello!' });
socket.on('message:new', (msg) => console.log(msg));
Clustering & Worker Threads
Advanced · 10 min read
const cluster = require('cluster');
const os = require('os');
// Cluster: spawn one worker per CPU core
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
console.log(`Primary ${process.pid}: forking ${numCPUs} workers`);
for (let i = 0; i < numCPUs; i++) cluster.fork();
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.pid} died. Forking replacement.`);
cluster.fork(); // auto-restart
});
} else {
startServer(); // each worker runs the Express app
}
// Worker Threads: CPU-intensive tasks
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename, { workerData: { n: 40 } });
worker.on('message', result => console.log('fib(40) =', result));
} else {
const { workerData } = require('worker_threads');
function fib(n) { return n < 2 ? n : fib(n-1) + fib(n-2); }
parentPort.postMessage(fib(workerData.n));
}
Deployment with PM2
Advanced · 8 min read
# Install PM2 globally
npm install -g pm2
# Start app
pm2 start src/index.js --name my-api
# Cluster mode (one process per CPU)
pm2 start src/index.js -i max --name my-api
# ecosystem.config.js — recommended
module.exports = {
apps: [{
name: 'my-api',
script: 'src/index.js',
instances: 'max',
exec_mode: 'cluster',
watch: false,
env: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '500M',
error_file: 'logs/err.log',
out_file: 'logs/out.log'
}]
};
pm2 start ecosystem.config.js
# PM2 commands
pm2 list # show all processes
pm2 logs my-api # tail logs
pm2 restart my-api # restart
pm2 reload my-api # zero-downtime reload
pm2 stop my-api # stop
pm2 delete my-api # remove
pm2 monit # live dashboard
pm2 startup # auto-start on server reboot
pm2 save # save process list