const http = require('http');
const express = require('express');
const jwt = require('jsonwebtoken');
app.listen(3000, () => {});
module.exports = router;
process.env.NODE_ENV
Node.js Zero to Hero · Backend Engineering

Backend
Engineering
with Node.js

From your first HTTP server to production-grade REST APIs, authentication systems, databases, and scalable backend architecture. Built for real-world engineers.

14Chapters
50+Code Examples
3Projects
v20 LTSNode.js
CH 01
Introduction to Node.js
What Node.js is, how it works under the hood, and when to use it
What is Node.js?

Node.js is a JavaScript runtime built on Chrome's V8 engine that lets you run JavaScript outside the browser — on servers, CLI tools, scripts, and beyond. It was created by Ryan Dahl in 2009 to solve the problem of concurrent connections in web servers.

V8 Engine

Google's open-source JavaScript engine that compiles JS to native machine code. Node.js embeds V8 and adds APIs for file system, networking, and OS access.

Use Cases

REST APIs · Real-time apps (WebSockets) · Microservices · CLI tools · Serverless functions · Backend for React/Vue/Next.js

Strengths

Non-blocking I/O · High concurrency · Single language full-stack · Massive npm ecosystem · Fast startup time

Weaknesses

Not ideal for CPU-intensive tasks (use worker threads) · Callback hell (solved by async/await) · Single-threaded by default

Event-Driven, Non-Blocking I/O

Node.js uses a single-threaded event loop combined with libuv's thread pool to handle thousands of concurrent connections without blocking. When you make a file read or DB query, Node delegates it and moves on — executing the callback when the operation finishes.

Your Code
JS callbacks
async/await
Event Loop
Call stack
Microtasks
Macrotasks
libuv
Thread pool
OS async I/O
I/O
Files · DB
Network · DNS
📌 Key Insight
Node's non-blocking model means one server process can handle 10,000 simultaneous database queries — whereas a traditional blocking server would need 10,000 threads. This is why Node.js excels at I/O-heavy backend work.
Installation & Environment Setup
terminal
bash
# Install Node.js v20 LTS via nvm (recommended)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 20
nvm use 20

# Verify installation
node --version   # v20.x.x
npm --version    # 10.x.x

# Run a JavaScript file directly
node index.js

# Start a REPL (interactive shell)
node
CH 02
Node.js Core Concepts
Modules, built-in APIs, and file system operations
Module Systems — CommonJS & ES Modules
modules/mathUtils.js — CommonJS (default)
js
// ── COMMONJS: require / module.exports ──────────────────
// math/operations.js
const add      = (a, b) => a + b;
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;

// Named exports object
module.exports = { add, subtract, multiply };

// ── OR: single export ───────────────────────────────────
module.exports = class Calculator {
  add(a, b)      { return a + b; }
  subtract(a, b) { return a - b; }
};

// ── Importing in another file ───────────────────────────
const { add, multiply } = require('./math/operations');
const path = require('path');       // built-in
const express = require('express'); // npm package

console.log(add(3, 4));  // 7

// ── ES MODULES: import / export (add "type":"module" to package.json) ──
// mathUtils.mjs
export const add = (a, b) => a + b;
export default function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}

// importing ESM
import divide, { add } from './mathUtils.mjs';
import * as utils from './mathUtils.mjs';
Built-in Modules — fs, path, os, http
builtins.js
js
const path = require('path');
const os   = require('os');

// ── PATH ────────────────────────────────────────────────
const fullPath = path.join(__dirname, 'data', 'users.json');
console.log(path.extname('server.js'));   // .js
console.log(path.basename('/api/users')); // users
console.log(path.dirname('/api/users/1')); // /api/users

// ── OS ──────────────────────────────────────────────────
console.log(os.platform());    // linux / darwin / win32
console.log(os.cpus().length); // number of CPU cores
console.log(os.freemem() / 1024 ** 3, 'GB free');

// ── HTTP (raw server) ───────────────────────────────────
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ status: 'ok' }));
});
server.listen(3000);
File System Operations
fileSystem.js
js
const fs   = require('fs');
const fsP  = require('fs/promises');  // promise-based API (recommended)
const path = require('path');

// ── ASYNC READ (promise-based) ──────────────────────────
async function readConfig() {
  try {
    const filePath = path.join(__dirname, 'config.json');
    const content  = await fsP.readFile(filePath, 'utf-8');
    return JSON.parse(content);
  } catch (err) {
    if (err.code === 'ENOENT') throw new Error('Config file not found');
    throw err;
  }
}

// ── WRITE FILE ──────────────────────────────────────────
async function saveData(filename, data) {
  await fsP.writeFile(filename, JSON.stringify(data, null, 2), 'utf-8');
  console.log(`Saved to ${filename}`);
}

// ── APPEND, DELETE, CHECK EXISTS ───────────────────────
await fsP.appendFile('logs/app.log', `[${new Date().toISOString()}] started\n`);
await fsP.unlink('temp.txt');              // delete
const exists = fs.existsSync('config.json'); // sync check

// ── READ DIRECTORY ──────────────────────────────────────
const files = await fsP.readdir('./');
files.forEach(f => console.log(f));
CH 03
NPM & Project Setup
Package management, scripts, and project structure
npm & package.json
terminal + package.json
bash
# Initialise a new project
npm init -y

# Install production dependency
npm install express         # saves to "dependencies"

# Install development dependency
npm install --save-dev nodemon jest

# Install globally (CLI tools)
npm install -g pm2

# Lock exact version
npm install express@4.18.2

# Remove package
npm uninstall express

# See installed packages
npm list --depth=0
package.json — production setup
json
{
  "name": "vendorflow-api",
  "version": "1.0.0",
  "description": "REST API backend",
  "main": "src/server.js",
  "scripts": {
    "start":   "node src/server.js",
    "dev":     "nodemon src/server.js",     // auto-restart on changes
    "test":    "jest --coverage",
    "lint":    "eslint src/**/*.js",
    "migrate": "node src/db/migrate.js",
    "seed":    "node src/db/seed.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "bcryptjs": "^2.4.3",
    "jsonwebtoken": "^9.0.0",
    "sequelize": "^6.35.0",
    "mysql2": "^3.6.0",
    "dotenv": "^16.3.1",
    "joi": "^17.11.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.2",
    "jest": "^29.7.0",
    "supertest": "^6.3.3"
  }
}
Project Structure — Clean Architecture
project folder structure
tree
vendorflow-api/
├── src/
│   ├── config/
│   │   ├── database.js        # DB connection config
│   │   └── app.js             # Express app setup
│   ├── controllers/           # Request handlers (thin layer)
│   │   ├── authController.js
│   │   ├── userController.js
│   │   └── paymentController.js
│   ├── services/              # Business logic
│   │   ├── authService.js
│   │   └── paymentService.js
│   ├── models/                # Sequelize / Mongoose models
│   │   ├── User.js
│   │   └── Payment.js
│   ├── routes/                # Route definitions
│   │   ├── auth.routes.js
│   │   ├── user.routes.js
│   │   └── index.js           # Combine all routes
│   ├── middlewares/
│   │   ├── auth.middleware.js  # JWT verification
│   │   ├── validate.js         # Joi validation
│   │   └── errorHandler.js
│   ├── utils/
│   │   ├── ApiError.js
│   │   ├── logger.js
│   │   └── helpers.js
│   └── server.js              # Entry point
├── tests/
│   ├── unit/
│   └── integration/
├── .env
├── .env.example
├── .gitignore
└── package.json
CH 04
Building a Basic HTTP Server
Raw HTTP server, manual routing, and JSON responses
Raw HTTP Server
server.js — native Node.js HTTP
js
const http = require('http');

const PORT = process.env.PORT || 3000;

const server = http.createServer(async (req, res) => {
  const { method, url } = req;

  // Parse JSON body from incoming request
  const parseBody = () => new Promise((resolve) => {
    let body = '';
    req.on('data', chunk => body += chunk.toString());
    req.on('end', () => resolve(body ? JSON.parse(body) : {}));
  });

  // Helper — send JSON response
  const json = (statusCode, data) => {
    res.writeHead(statusCode, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(data));
  };

  // Routing
  if (method === 'GET'  && url === '/')
    return json(200, { message: 'Node.js API running!' });

  if (method === 'POST' && url === '/echo') {
    const body = await parseBody();
    return json(200, { received: body });
  }

  json(404, { error: 'Route not found' });
});

server.listen(PORT, () =>
  console.log(`Server running on http://localhost:${PORT}`)
);
💡 Why Learn Raw HTTP First?
Understanding the raw HTTP module helps you appreciate what Express does for you. In production, always use Express or Fastify — they handle routing, body parsing, and middleware much more elegantly.
CH 05
Express.js
Middleware, routing, request/response handling
Express App Setup
src/config/app.js
js
const express = require('express');
const cors    = require('cors');
const helmet  = require('helmet');  // security headers
const morgan  = require('morgan'); // request logging
const routes  = require('../routes');

const app = express();

// ── GLOBAL MIDDLEWARE ───────────────────────────────────
app.use(helmet());           // Set security HTTP headers
app.use(cors({
  origin: process.env.CLIENT_URL || '*',
  credentials: true,
}));
app.use(morgan('combined'));  // Log all requests
app.use(express.json({ limit: '10mb' }));      // Parse JSON body
app.use(express.urlencoded({ extended: true })); // Parse form data

// ── ROUTES ──────────────────────────────────────────────
app.use('/api/v1', routes);

// ── HEALTH CHECK ────────────────────────────────────────
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date() });
});

// ── 404 HANDLER ─────────────────────────────────────────
app.use('*', (req, res) => {
  res.status(404).json({ success: false, message: 'Route not found' });
});

module.exports = app;
Custom Middleware
middlewares/auth.middleware.js
js
const jwt = require('jsonwebtoken');

// Middleware: a function with (req, res, next) signature
// Call next() to pass control to the next middleware/route

const authenticate = (req, res, next) => {
  try {
    const authHeader = req.headers.authorization;
    if (!authHeader?.startsWith('Bearer ')) {
      return res.status(401).json({ success: false, message: 'No token provided' });
    }

    const token   = authHeader.split(' ')[1];
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    req.user = decoded;  // attach user to request
    next();            // continue to route handler
  } catch (error) {
    res.status(401).json({ success: false, message: 'Invalid or expired token' });
  }
};

// Role-based access — factory middleware
const authorize = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user?.role)) {
    return res.status(403).json({ success: false, message: 'Insufficient permissions' });
  }
  next();
};

// Request logger middleware
const requestLogger = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next();
};

module.exports = { authenticate, authorize, requestLogger };
Routing — GET, POST, PUT, DELETE
routes/user.routes.js
js
const express    = require('express');
const router     = express.Router();
const controller = require('../controllers/userController');
const { authenticate, authorize } = require('../middlewares/auth.middleware');
const validate   = require('../middlewares/validate');
const { updateUserSchema } = require('../validators/user.validator');

// GET /api/v1/users?page=1&limit=10&role=admin
router.get('/', authenticate, controller.getAll);

// GET /api/v1/users/:id
router.get('/:id', authenticate, controller.getById);

// PUT /api/v1/users/:id — with validation middleware
router.put('/:id', authenticate, validate(updateUserSchema), controller.update);

// DELETE /api/v1/users/:id — admin only
router.delete('/:id', authenticate, authorize('admin'), controller.delete);

module.exports = router;
CH 06
REST API Development
Designing RESTful APIs, CRUD, status codes, and validation
REST Design Principles
MethodPathActionStatus
GET/api/v1/usersList all users (with pagination)200
GET/api/v1/users/:idGet single user200 / 404
POST/api/v1/usersCreate new user201
PUT/api/v1/users/:idReplace user (full update)200 / 404
PATCH/api/v1/users/:idPartial update200 / 404
DELETE/api/v1/users/:idDelete user204 / 404
CRUD Controller — Clean Pattern
controllers/userController.js
js
const userService = require('../services/userService');
const { ApiError } = require('../utils/ApiError');

// Thin controller — delegates to service layer

const getAll = async (req, res, next) => {
  try {
    const { page = 1, limit = 10, role, search } = req.query;
    const result = await userService.findAll({
      page:   parseInt(page),
      limit:  parseInt(limit),
      role, search
    });
    res.json({ success: true, data: result });
  } catch (err) { next(err); }
};

const getById = async (req, res, next) => {
  try {
    const user = await userService.findById(req.params.id);
    if (!user) throw new ApiError(404, 'User not found');
    res.json({ success: true, data: user });
  } catch (err) { next(err); }
};

const update = async (req, res, next) => {
  try {
    const updated = await userService.update(req.params.id, req.body);
    if (!updated) throw new ApiError(404, 'User not found');
    res.json({ success: true, data: updated, message: 'User updated' });
  } catch (err) { next(err); }
};

const remove = async (req, res, next) => {
  try {
    await userService.delete(req.params.id);
    res.status(204).send();
  } catch (err) { next(err); }
};

module.exports = { getAll, getById, update, remove };
CH 07
Database Integration
MySQL with Sequelize ORM and MongoDB with Mongoose
MySQL + Sequelize ORM
config/database.js + models/User.js
js
// ── DATABASE CONNECTION ──────────────────────────────────
const { Sequelize } = require('sequelize');
require('dotenv').config();

const sequelize = new Sequelize(
  process.env.DB_NAME,
  process.env.DB_USER,
  process.env.DB_PASSWORD,
  {
    host:    process.env.DB_HOST,
    dialect: 'mysql',
    pool:    { max: 10, min: 0, acquire: 30000, idle: 10000 },
    logging: process.env.NODE_ENV === 'development' ? console.log : false,
  }
);

module.exports = sequelize;

// ── USER MODEL ──────────────────────────────────────────
const { DataTypes, Model } = require('sequelize');
const sequelize = require('../config/database');

class User extends Model {}

User.init({
  id:       { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
  name:     { type: DataTypes.STRING(100),   allowNull: false },
  email:    { type: DataTypes.STRING,   allowNull: false, unique: true },
  password: { type: DataTypes.STRING,   allowNull: false },
  role:     { type: DataTypes.ENUM('user', 'admin'), defaultValue: 'user' },
  active:   { type: DataTypes.BOOLEAN,  defaultValue: true },
}, {
  sequelize,
  modelName:  'User',
  tableName:  'users',
  timestamps: true,                    // adds createdAt, updatedAt
  paranoid:   true,                    // soft delete (adds deletedAt)
  hooks: {
    beforeCreate: async (user) => {
      user.email = user.email.toLowerCase();
    },
  },
});

module.exports = User;
MongoDB + Mongoose
models/Product.js — Mongoose schema
js
const mongoose = require('mongoose');

const productSchema = new mongoose.Schema({
  name:        { type: String,   required: [true, 'Product name is required'], trim: true },
  description: { type: String,   maxLength: 1000 },
  price:       { type: Number,   required: true, min: 0 },
  category:    { type: String,   enum: ['electronics', 'clothing', 'food'] },
  stock:       { type: Number,   default: 0, min: 0 },
  images:      [{ type: String }],
  seller:      { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  tags:        [String],
  active:      { type: Boolean,  default: true },
}, {
  timestamps: true,  // auto createdAt & updatedAt
  toJSON: { virtuals: true },
});

// Virtual — computed field, not stored in DB
productSchema.virtual('isInStock').get(function() {
  return this.stock > 0;
});

// Index for fast search
productSchema.index({ name: 'text', description: 'text' });
productSchema.index({ seller: 1, category: 1 });

module.exports = mongoose.model('Product', productSchema);
Database CRUD via Service Layer
services/userService.js
js
const { Op } = require('sequelize');
const User     = require('../models/User');
const { ApiError } = require('../utils/ApiError');

const findAll = async ({ page, limit, role, search }) => {
  const where = {};
  if (role)   where.role   = role;
  if (search) where.name   = { [Op.iLike]: `%${search}%` };

  const { count, rows } = await User.findAndCountAll({
    where,
    attributes: { exclude: ['password'] },  // never expose password
    limit,
    offset: (page - 1) * limit,
    order:  [['createdAt', 'DESC']],
  });

  return {
    users:       rows,
    total:       count,
    pages:       Math.ceil(count / limit),
    currentPage: page,
  };
};

const findById = async (id) => {
  return User.findByPk(id, { attributes: { exclude: ['password'] } });
};

const update = async (id, data) => {
  const user = await User.findByPk(id);
  if (!user) return null;
  return user.update(data);
};

const deleteUser = async (id) => {
  const user = await User.findByPk(id);
  if (!user) throw new ApiError(404, 'User not found');
  await user.destroy();  // soft delete if paranoid:true
};

module.exports = { findAll, findById, update, delete: deleteUser };
CH 08
Authentication & Security
Register/login, bcrypt, JWT, and backend security hardening
Register & Login System
services/authService.js
js
const bcrypt = require('bcryptjs');
const jwt    = require('jsonwebtoken');
const User   = require('../models/User');
const { ApiError } = require('../utils/ApiError');

const register = async ({ name, email, password }) => {
  // 1. Check if email already exists
  const existing = await User.findOne({ where: { email } });
  if (existing) throw new ApiError(409, 'Email already registered');

  // 2. Hash password — saltRounds 12 is the production standard
  const hashedPassword = await bcrypt.hash(password, 12);

  // 3. Create user
  const user = await User.create({
    name, email,
    password: hashedPassword,
  });

  // 4. Return user without password
  const { password: _, ...safeUser } = user.toJSON();
  return { user: safeUser, token: generateToken(user) };
};

const login = async ({ email, password }) => {
  // Find user — include password for comparison
  const user = await User.findOne({ where: { email } });

  // Generic error — don't reveal which field was wrong
  if (!user) throw new ApiError(401, 'Invalid credentials');

  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) throw new ApiError(401, 'Invalid credentials');

  if (!user.active) throw new ApiError(403, 'Account suspended');

  const { password: _, ...safeUser } = user.toJSON();
  return { user: safeUser, token: generateToken(user) };
};

const generateToken = (user) => jwt.sign(
  { id: user.id, role: user.role, email: user.email },
  process.env.JWT_SECRET,
  { expiresIn: process.env.JWT_EXPIRES || '7d', algorithm: 'HS256' }
);

module.exports = { register, login };
JWT Token Flow
Client
POST /login
email + password
Auth Service
Verify bcrypt
Sign JWT
Response
{ token }
expires 7d
Protected Route
Bearer token
→ verify → allow
⚠️ JWT Security
Store JWTs in HttpOnly cookies (not localStorage) in browser apps — this prevents XSS attacks from stealing tokens. In mobile apps, use secure storage. Always validate iss and aud claims in production.
Security Best Practices
security hardening checklist
js
// ── 1. PARAMETERISED QUERIES (prevents SQL injection) ───
// ❌ NEVER do this:
const sql = `SELECT * FROM users WHERE email = '${email}'`;

// ✅ ALWAYS use Sequelize (parameterised automatically) or:
User.findOne({ where: { email } });   // Sequelize
db.query('SELECT * WHERE email = ?', [email]); // Raw SQL

// ── 2. INPUT VALIDATION WITH JOI ────────────────────────
const Joi = require('joi');
const registerSchema = Joi.object({
  name:     Joi.string().min(2).max(50).required(),
  email:    Joi.string().email().required(),
  password: Joi.string().min(8).pattern(/^(?=.*[A-Z])(?=.*\d)/).required(),
});

// ── 3. RATE LIMITING ────────────────────────────────────
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 5,                    // 5 attempts per window
  message: 'Too many login attempts',
  standardHeaders: true,
});
app.use('/api/auth/login', loginLimiter);

// ── 4. HELMET (HTTP security headers) ───────────────────
app.use(helmet());
// Sets: X-Content-Type-Options, X-Frame-Options,
// Content-Security-Policy, HSTS, and more

// ── 5. SANITIZE INPUT (prevent XSS) ─────────────────────
const xss = require('xss-clean');
app.use(xss());   // strips HTML tags from req.body/query/params
CH 09
Error Handling & Middleware
Custom errors, global handlers, logging
Custom Error Classes
utils/ApiError.js
js
// Centralised error class — extends native Error
class ApiError extends Error {
  constructor(statusCode, message, errors = [], stack = '') {
    super(message);
    this.statusCode = statusCode;
    this.success    = false;
    this.errors     = errors;

    if (stack) {
      this.stack = stack;
    } else {
      Error.captureStackTrace(this, this.constructor);
    }
  }

  // Factory methods for common errors
  static notFound(msg = 'Resource not found')   { return new ApiError(404, msg); }
  static unauthorized(msg = 'Unauthorized')        { return new ApiError(401, msg); }
  static forbidden(msg = 'Forbidden')             { return new ApiError(403, msg); }
  static badRequest(msg = 'Bad request')           { return new ApiError(400, msg); }
  static conflict(msg = 'Resource already exists') { return new ApiError(409, msg); }
}

module.exports = { ApiError };
Global Error Handler Middleware
middlewares/errorHandler.js
js
const { ApiError } = require('../utils/ApiError');
const logger       = require('../utils/logger');

// Express error middleware — MUST have 4 parameters
const errorHandler = (err, req, res, next) => {
  let error = { ...err };
  error.message = err.message;

  // Log all errors in development
  if (process.env.NODE_ENV === 'development') {
    logger.error(`${err.statusCode || 500} - ${err.message} - ${req.originalUrl}`);
  }

  // Sequelize validation error
  if (err.name === 'SequelizeValidationError') {
    const message = err.errors.map(e => e.message).join(', ');
    error = new ApiError(400, message);
  }

  // Sequelize unique constraint
  if (err.name === 'SequelizeUniqueConstraintError') {
    error = new ApiError(409, 'Duplicate field value');
  }

  // JWT errors
  if (err.name === 'JsonWebTokenError')   error = new ApiError(401, 'Invalid token');
  if (err.name === 'TokenExpiredError')    error = new ApiError(401, 'Token expired');

  res.status(error.statusCode || 500).json({
    success: false,
    message: error.message || 'Internal Server Error',
    errors:  error.errors  || [],
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
  });
};

module.exports = errorHandler;

// Register AFTER all routes in app.js:
// app.use(errorHandler);
CH 10
Advanced Node.js Concepts
Async patterns, streams, buffers, and environment config
Async Patterns — Callbacks → Promises → Async/Await
asyncPatterns.js
js
// ── 1. CALLBACKS (legacy — avoid in new code) ───────────
fs.readFile('file.txt', 'utf-8', (err, data) => {
  if (err) return console.error(err);
  console.log(data);
});

// ── 2. PROMISES ──────────────────────────────────────────
fsP.readFile('file.txt', 'utf-8')
  .then(data  => console.log(data))
  .catch(err   => console.error(err))
  .finally(()  => console.log('Done'));

// ── 3. ASYNC/AWAIT (recommended) ────────────────────────
async function processFile(filename) {
  try {
    const content = await fsP.readFile(filename, 'utf-8');
    const parsed  = JSON.parse(content);
    return parsed;
  } catch (err) {
    throw new Error(`Failed to process ${filename}: ${err.message}`);
  }
}

// ── PARALLEL EXECUTION ───────────────────────────────────
async function fetchAllData(userId) {
  // Promise.all — runs concurrently, fails fast if any rejects
  const [user, orders, profile] = await Promise.all([
    User.findByPk(userId),
    Order.findAll({ where: { userId } }),
    Profile.findOne({ where: { userId } }),
  ]);
  return { user, orders, profile };
}

// Promise.allSettled — collect all results, don't fail fast
const results = await Promise.allSettled([
  fetchUser(), fetchOrders(), fetchInventory()
]);
results.forEach(r => {
  if (r.status === 'fulfilled') console.log(r.value);
  else                              console.error(r.reason);
});
Streams — Efficient Data Processing
streams.js — CSV upload + processing
js
const fs     = require('fs');
const csv    = require('csv-parser');
const { pipeline } = require('stream/promises');
const { Transform } = require('stream');

// ── PIPE: read → transform → write ─────────────────────
const processLargeFile = async (inputPath, outputPath) => {
  const reader    = fs.createReadStream(inputPath);
  const writer    = fs.createWriteStream(outputPath);

  const upperCase = new Transform({
    transform(chunk, encoding, callback) {
      this.push(chunk.toString().toUpperCase());
      callback();
    }
  });

  // pipeline handles backpressure and cleanup
  await pipeline(reader, upperCase, writer);
  console.log('File processed successfully');
};

// ── STREAM CSV FILE ─────────────────────────────────────
const importUsersFromCSV = (filePath) => new Promise((resolve, reject) => {
  const users = [];

  fs.createReadStream(filePath)
    .pipe(csv())
    .on('data', row => users.push({ name: row.name, email: row.email }))
    .on('end', () => resolve(users))
    .on('error', reject);
});

// ── STREAM LARGE API RESPONSE ───────────────────────────
app.get('/export/users', authenticate, async (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.write('[');
  let first = true;

  const cursor = User.findAll({ raw: true, attributes: ['id', 'name', 'email'] });
  for await (const user of cursor) {
    if (!first) res.write(',');
    res.write(JSON.stringify(user));
    first = false;
  }
  res.end(']');
});
Environment Variables — dotenv
.env.example + config/env.js
bash
# .env.example — commit this, NOT .env!
NODE_ENV=development
PORT=3000

DB_HOST=localhost
DB_PORT=3306
DB_NAME=vendorflow_db
DB_USER=root
DB_PASSWORD=

JWT_SECRET=change-this-to-a-64-char-random-string
JWT_EXPIRES=7d

REDIS_URL=redis://localhost:6379
CLIENT_URL=http://localhost:5173
config/env.js — validated config
js
require('dotenv').config();

// Validate all required env vars at startup
const required = [
  'DB_NAME', 'DB_USER', 'DB_PASSWORD',
  'JWT_SECRET', 'NODE_ENV'
];

required.forEach(key => {
  if (!process.env[key]) {
    console.error(`❌ Missing required env var: ${key}`);
    process.exit(1);   // crash early, loudly
  }
});

module.exports = {
  env:        process.env.NODE_ENV || 'development',
  port:       parseInt(process.env.PORT) || 3000,
  jwtSecret:  process.env.JWT_SECRET,
  jwtExpires: process.env.JWT_EXPIRES || '7d',
  db: {
    host:     process.env.DB_HOST,
    name:     process.env.DB_NAME,
    user:     process.env.DB_USER,
    password: process.env.DB_PASSWORD,
  },
};
CH 11
Testing
Unit testing with Jest and API integration testing with Supertest
Unit Testing with Jest
tests/unit/authService.test.js
js
const bcrypt      = require('bcryptjs');
const authService = require('../../src/services/authService');
const User        = require('../../src/models/User');

// Mock the database model
jest.mock('../../src/models/User');

describe('AuthService', () => {
  beforeEach(() => jest.clearAllMocks());

  describe('register', () => {
    it('should register a new user successfully', async () => {
      User.findOne.mockResolvedValue(null);  // no existing user
      User.create.mockResolvedValue({
        id: 'uuid-123', name: 'Alice',
        email: 'alice@test.com',
        toJSON: () => ({ id: 'uuid-123', name: 'Alice' })
      });

      const result = await authService.register({
        name: 'Alice', email: 'alice@test.com', password: 'SecurePass1!'
      });

      expect(result).toHaveProperty('token');
      expect(result.user).not.toHaveProperty('password');
    });

    it('should throw 409 if email already exists', async () => {
      User.findOne.mockResolvedValue({ id: 'existing' });

      await expect(authService.register({
        name: 'Bob', email: 'taken@test.com', password: 'Pass123!'
      })).rejects.toMatchObject({ statusCode: 409 });
    });
  });
});
API Integration Testing — Supertest
tests/integration/auth.test.js
js
const request = require('supertest');
const app     = require('../../src/config/app');
const sequelize = require('../../src/config/database');

beforeAll(async () => {
  await sequelize.sync({ force: true }); // reset test DB
});

afterAll(async () => {
  await sequelize.close();
});

describe('POST /api/v1/auth/register', () => {
  it('should create a new user and return token', async () => {
    const res = await request(app)
      .post('/api/v1/auth/register')
      .send({ name: 'Alice', email: 'alice@test.com', password: 'SecurePass1!' })
      .expect(201);

    expect(res.body.success).toBe(true);
    expect(res.body.data).toHaveProperty('token');
    expect(res.body.data.user).not.toHaveProperty('password');
  });

  it('should reject invalid email', async () => {
    const res = await request(app)
      .post('/api/v1/auth/register')
      .send({ name: 'Bob', email: 'not-an-email', password: 'pass' })
      .expect(400);

    expect(res.body.success).toBe(false);
  });
});
CH 12
Performance & Scalability
Redis caching, rate limiting, and API optimisation
Redis Caching Strategy
utils/cache.js + middleware/cacheMiddleware.js
js
const redis = require('ioredis');
const client = new redis(process.env.REDIS_URL);

const cache = {
  async get(key) {
    const data = await client.get(key);
    return data ? JSON.parse(data) : null;
  },
  async set(key, value, ttlSeconds = 300) {
    await client.setex(key, ttlSeconds, JSON.stringify(value));
  },
  async del(key)         { await client.del(key); },
  async invalidate(pattern) {
    const keys = await client.keys(pattern);
    if (keys.length) await client.del(...keys);
  },
};

// Cache middleware factory
const cacheMiddleware = (keyFn, ttl = 300) => async (req, res, next) => {
  const key = keyFn(req);
  const cached = await cache.get(key);

  if (cached) {
    return res.json({ ...cached, fromCache: true });
  }

  // Intercept response to cache it
  const originalJson = res.json.bind(res);
  res.json = (data) => {
    cache.set(key, data, ttl).catch(console.error);
    return originalJson(data);
  };

  next();
};

// Usage on route:
// router.get('/', cacheMiddleware(req => `users:page:${req.query.page}`, 120), controller.getAll);

module.exports = { cache, cacheMiddleware };
Rate Limiting & API Optimisation
config/rateLimits.js
js
const rateLimit    = require('express-rate-limit');
const RedisStore   = require('rate-limit-redis');
const { client }   = require('./redis');

// Redis-backed rate limiter (works across multiple servers)
const createLimiter = (max, windowMinutes, message) => rateLimit({
  windowMs: windowMinutes * 60 * 1000,
  max,
  message:  { success: false, message },
  standardHeaders: true,
  legacyHeaders:   false,
  store: new RedisStore({ sendCommand: (...args) => client.call(...args) }),
  keyGenerator: (req) => req.user?.id || req.ip, // per-user when authenticated
});

module.exports = {
  globalLimiter: createLimiter(100, 15, 'Too many requests'),
  authLimiter:   createLimiter(5,   15, 'Too many auth attempts'),
  apiLimiter:    createLimiter(30,  1,  'API rate limit exceeded'),
};
CH 13
Deployment & DevOps
Production setup, Docker, environment config, and CI/CD
Production Server Setup with PM2
ecosystem.config.js — PM2 config
js
module.exports = {
  apps: [{
    name:         'vendorflow-api',
    script:       './src/server.js',
    instances:    'max',           // one per CPU core
    exec_mode:    'cluster',        // cluster mode for load balancing
    watch:        false,
    max_memory_restart: '500M',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
    log_date_format: 'YYYY-MM-DD HH:mm:ss',
    error_file:  'logs/error.log',
    out_file:    'logs/out.log',
    merge_logs:  true,
  }],
};

# Start, monitor, restart:
# pm2 start ecosystem.config.js --env production
# pm2 monit
# pm2 reload vendorflow-api
Docker — Containerised Deployment
Dockerfile + docker-compose.yml
docker
# ── Multi-stage Dockerfile ──────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
docker-compose.yml
yaml
version: '3.9'
services:
  api:
    build: .
    ports:    ["3000:3000"]
    depends_on: [mysql, redis]
    env_file: .env
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: vendorflow_db
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
    volumes: [mysql_data:/var/lib/mysql]
    ports: ["3306:3306"]

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes: [redis_data:/data]

volumes:
  mysql_data:
  redis_data:
CI/CD with GitHub Actions
.github/workflows/deploy.yml
yaml
name: Test and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env: { MYSQL_ROOT_PASSWORD: test, MYSQL_DATABASE: test_db }
        options: --health-cmd="mysqladmin ping" --health-interval=10s

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm test -- --coverage
      - run: npm run lint

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Deploy to VPS
        uses: appleboy/ssh-action@v1
        with:
          host:     ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key:      ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /app/vendorflow-api
            git pull origin main
            npm ci --only=production
            pm2 reload vendorflow-api
CH 14
Mini Projects
Three real-world builds from beginner to advanced production system
Project Overview
Beginner
CRUD REST API

Full REST API with in-memory and file-based storage. Products, categories, search, pagination.

Node.js Express JSON files
Intermediate
Auth System

Register/login with JWT, roles, profile management, email verification flow.

Express MySQL Sequelize bcrypt + JWT
Advanced
VendorFlow API

Fintech backend: multi-vendor payments, fraud detection hooks, webhook system, admin dashboard API.

Express MySQL Redis JWT Docker
VendorFlow — Advanced Fintech API
VendorFlow project structure
tree
vendorflow-api/
├── src/
│   ├── config/
│   │   ├── app.js           # Express + all middleware
│   │   ├── database.js      # Sequelize connection + pool
│   │   ├── redis.js         # ioredis client
│   │   └── env.js           # Validated env vars
│   ├── models/
│   │   ├── User.js          # id, role, email, kycStatus
│   │   ├── Vendor.js        # businessName, accountNumber
│   │   ├── Payment.js       # amount, currency, status, txRef
│   │   ├── Wallet.js        # balance, currency, userId
│   │   └── Webhook.js       # url, secret, events
│   ├── controllers/
│   │   ├── authController.js
│   │   ├── paymentController.js
│   │   └── vendorController.js
│   ├── services/
│   │   ├── authService.js
│   │   ├── paymentService.js   # core payment logic
│   │   ├── walletService.js    # credit/debit + ledger
│   │   ├── fraudService.js     # velocity checks, rules engine
│   │   └── webhookService.js   # deliver events to vendors
│   ├── routes/
│   │   ├── auth.routes.js
│   │   ├── payment.routes.js
│   │   ├── vendor.routes.js
│   │   └── index.js
│   ├── middlewares/
│   │   ├── auth.middleware.js
│   │   ├── validate.js
│   │   ├── idempotency.js       # prevent duplicate payments
│   │   └── errorHandler.js
│   └── server.js
├── tests/
├── Dockerfile
├── docker-compose.yml
└── .github/workflows/deploy.yml
services/paymentService.js — production payment logic
js
const { sequelize } = require('../config/database');
const Payment      = require('../models/Payment');
const Wallet       = require('../models/Wallet');
const fraudService = require('./fraudService');
const webhookService = require('./webhookService');
const { ApiError } = require('../utils/ApiError');

const initiatePayment = async ({ senderId, recipientId, amount, currency, reference }) => {
  // 1. Fraud check before processing
  const fraudCheck = await fraudService.evaluate({ senderId, amount, currency });
  if (fraudCheck.blocked) {
    throw new ApiError(403, `Transaction blocked: ${fraudCheck.reason}`);
  }

  // 2. Run in database transaction — atomicity guaranteed
  const result = await sequelize.transaction(async (t) => {
    // Lock wallet rows to prevent race conditions
    const senderWallet = await Wallet.findOne({
      where: { userId: senderId, currency },
      lock:  t.LOCK.UPDATE,  // SELECT FOR UPDATE
      transaction: t,
    });

    if (!senderWallet || senderWallet.balance < amount) {
      throw new ApiError(422, 'Insufficient wallet balance');
    }

    const recipientWallet = await Wallet.findOne({
      where: { userId: recipientId, currency },
      lock: t.LOCK.UPDATE,
      transaction: t,
    });

    // Atomic debit + credit
    await senderWallet.decrement('balance', { by: amount, transaction: t });
    await recipientWallet.increment('balance', { by: amount, transaction: t });

    const payment = await Payment.create({
      senderId, recipientId, amount, currency,
      reference, status: 'completed',
      riskScore: fraudCheck.riskScore,
    }, { transaction: t });

    return payment;
  });

  // 3. Fire webhook asynchronously (don't block response)
  webhookService.dispatch('payment.completed', result).catch(console.error);

  return result;
};

module.exports = { initiatePayment };
✅ Production-Ready Checklist
Helmet security headers · CORS configured · Rate limiting on all routes · Input validation (Joi) · Parameterised queries · JWT in HttpOnly cookies · Centralised error handler · Environment variable validation · Docker containerised · PM2 cluster mode · GitHub Actions CI/CD · Logging with Winston or Pino