Module 5

Express & REST APIs

Express Framework • Routing • Middleware • Error Handling • Validation • Logging • API Patterns

🚀 Building Real APIs with Express

In Module 4, you built HTTP servers from scratch with Node's http module — manual routing, manual body parsing, manual everything. Express is the most popular web framework for Node.js that handles all of that elegantly.

💡 Remember Module 4? You wrote if/else routing, manually collected chunks for POST bodies, and handled status codes by hand. Express simplifies ALL of that while giving you powerful new features like middleware!

🎯 By the end of this module, you will:

  • Set up an Express server with clean routing
  • Handle route parameters, query strings, and request bodies
  • Understand the middleware pipeline and write custom middleware
  • Build a complete REST API with full CRUD operations
  • Master error handling (6-part deep dive: from crashes to production-ready)
  • Validate input with express-validator
  • Implement professional logging with Winston
  • Design consistent API response patterns

Express Framework

Routing, middleware, body parsing

🔄

REST API

Full CRUD with proper HTTP methods

🛡️

Error Handling

6-part guide: crash → production-ready

Validation & Logging

express-validator, Winston, API patterns

⚡ Express Setup

Express is a minimal, flexible Node.js web framework. Install it and create your first server in seconds:

Bash
# Initialize project & install Express
npm init -y
npm install express
npm install -D nodemon

🏗️ Your First Express Server

Just three lines to create a working web server: import Express, create an app, start listening. Compare this to the 15+ lines you needed with the raw http module in Module 4! Each line of the code below is commented to explain what it does:

JavaScript
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Parse JSON bodies (for POST/PUT requests)
app.use(express.json());

// Your first route
app.get('/', (req, res) => {
    res.json({ message: 'Hello from Express!' });
});

// Start the server
app.listen(PORT, () => {
    console.log(`🚀 Server running at http://localhost:${PORT}`);
});
Terminal
# Terminal
$ npx nodemon server.js
🚀 Server running at http://localhost:3000

$ curl http://localhost:3000/
{"message":"Hello from Express!"}

🔄 http Module vs Express

See how much simpler Express makes everything compared to the raw http module from Module 4:

The http module requires you to manually check URLs, set headers, and handle status codes. Express abstracts all of that away — you just declare your routes and handlers. This isn't just about less code; it's about fewer bugs, faster development, and cleaner architecture:

❌ With http module (verbose)
JavaScript
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World');
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});

server.listen(3000);
✅ With Express (clean & simple)
JavaScript
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(3000);

✨ Much Cleaner! No http.createServer(), no manual req.url parsing, no res.writeHead(). Express handles status codes, headers, and routing automatically — you focus on the logic!

🛣️ Routing & Parameters

Express routing maps HTTP methods and URL patterns to handler functions. No more if/else chains!

💡 What is Routing?

Think of routing as a reception desk at a hotel. When a guest (HTTP request) arrives, the receptionist (Express router) checks what they need (the URL path and HTTP method) and directs them to the right room (handler function). Without routing, you'd have one giant room handling everything — that's what raw if/else chains in the http module felt like!

📝 HTTP Methods → Express Methods

Each HTTP method has a specific semantic meaning. Using the right method tells other developers (and clients) what the endpoint does, even before reading the code. Express provides a matching method for each one:

JavaScript
app.get('/users', handler);       // Read
app.post('/users', handler);      // Create
app.put('/users/:id', handler);   // Update (full replace)
app.patch('/users/:id', handler); // Update (partial)
app.delete('/users/:id', handler);// Delete

🔑 Route Parameters (:id)

Route parameters are dynamic segments in the URL path. They let you capture values from the URL itself. When you write /users/:id, the :id part becomes a variable — Express extracts it and puts it in req.params. This is how you access a specific resource (one user, one product, one order).

JavaScript
app.get('/users/:id', (req, res) => {
    const userId = req.params.id;  // "42"
    res.json({ id: userId });
});
// GET /users/42 → { id: "42" }

❓ Query Strings (?key=value)

Query strings are optional filters added after the ? in a URL. Unlike route parameters (which identify which resource), query strings modify how you want the data — filtering, sorting, pagination, searching. Express automatically parses them into req.query as an object.

JavaScript
app.get('/search', (req, res) => {
    const { q, page = 1 } = req.query;
    res.json({ query: q, page });
});
// GET /search?q=node&page=2 → { query: "node", page: "2" }

📂 Express Router (Organize Routes)

As your app grows, putting all routes in one file becomes unmanageable. express.Router() lets you create mini-applications — each with its own routes — that you can mount onto your main app. Think of it like organizing files into folders instead of dumping everything on your desktop.

JavaScript
// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', getAllUsers);
router.post('/', createUser);
router.get('/:id', getUserById);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);

module.exports = router;

// server.js
const userRoutes = require('./routes/users');
app.use('/api/users', userRoutes);
// All routes prefixed with /api/users

🔑 params vs query — When to Use Which?

req.params → identifies which resource: /users/42 (get user 42). req.query → modifies how to get data: /users?role=admin&sort=name (filter & sort). Rule of thumb: if it's required to find the resource, use params. If it's optional (filtering, sorting, pagination), use query strings.

⚠️ Route Order Matters!

Express matches routes top to bottom. If you define app.get('/users/new') AFTER app.get('/users/:id'), then /users/new will match :id = "new" instead! Always put specific routes before dynamic ones.

📦 Body Parsing

Express provides built-in middleware to parse request bodies. Remember collecting chunks manually in Module 4? Express does it all for you!

💡 What is a Request Body?

When a client sends data to your server (submitting a form, creating a user, uploading JSON), that data travels in the body of the HTTP request. GET requests typically have no body (they use query strings instead), while POST, PUT, and PATCH requests carry data in the body. By default, Express does not parse the body — you need to tell it how with middleware!

📦 express.json() — Parse JSON Bodies

This is the most commonly used body parser. It reads incoming JSON data (from API clients, fetch requests, mobile apps) and converts it into a JavaScript object available on req.body. Without this middleware, req.body would be undefined!

JavaScript
app.use(express.json());

app.post('/users', (req, res) => {
    const userData = req.body; // ← Automatically parsed!
    console.log('Received:', userData);
    res.status(201).json({ message: 'User created', user: userData });
});

// POST /users  Body: { "name": "Alice", "email": "alice@example.com" }
// → { "message": "User created", "user": { "name": "Alice", ... } }

💡 Remember the http module?

With raw http, you had to manually listen to data and end events, concatenate chunks, and parse JSON. Express does all of that for you with express.json()! 🎉

📝 express.text() — Parse Plain Text

Sometimes clients send plain text instead of structured data (log messages, raw content, webhooks). This parser makes req.body a simple string instead of an object.

JavaScript
app.use(express.text());

app.post('/message', (req, res) => {
    console.log('Received text:', req.body); // A string, not an object!
    res.send(`You sent: ${req.body}`);
});

📋 express.urlencoded() — Parse Form Data

HTML forms submit data in URL-encoded format (key1=value1&key2=value2). This parser handles traditional form submissions. The { extended: true } option allows nested objects in form data (using the qs library), while false only supports flat key-value pairs.

JavaScript
app.use(express.urlencoded({ extended: true }));

app.post('/login', (req, res) => {
    console.log(req.body); // { username: 'alice', password: '12345' }
    res.send(`Welcome ${req.body.username}`);
});
// Form: username=alice&password=12345

🌐 express.static() — Serve Static Files

Not technically a body parser, but an essential built-in middleware! express.static() serves files directly from a folder — HTML, CSS, JavaScript, images — without writing any route handlers. Express checks the folder first; if a matching file exists, it sends it. Otherwise, the request continues to your routes.

JavaScript
// Serve all files from 'public' folder
app.use(express.static('public'));
// http://localhost:3000/index.html
// http://localhost:3000/style.css

// With virtual path prefix
app.use('/assets', express.static('public'));
// http://localhost:3000/assets/style.css

// Multiple folders
app.use(express.static('public'));
app.use(express.static('uploads'));
// Express checks 'public' first, then 'uploads'

// With cache headers
app.use(express.static('public', {
    maxAge: '1d', etag: true
}));

⚠️ Body Parser Comparison

express.json()

req.bodyobject (parsed JSON)

express.text()

req.bodystring (plain text)

express.urlencoded()

req.bodyobject (form data)

💡 Pro Tip: Use Multiple Parsers Together

In most real apps, you'll use both express.json() and express.urlencoded() together — JSON for API clients and URL-encoded for HTML forms. Each parser only processes requests with a matching Content-Type header, so they don't interfere with each other!

🔗 Middleware: The Request Pipeline

Middleware is one of Express's most powerful features. Think of it as a queue of filters that every request passes through before reaching your route handler.

🎯 What is Middleware?

Middleware are functions that have access to the request (req), response (res), and the next function. Each middleware can:

  • ✅ Execute any code
  • ✅ Modify req and res objects
  • ✅ End the request-response cycle
  • ✅ Call next() to pass control to the next middleware

🔄 Request Flow: The Middleware Queue

Every time a client sends an HTTP request to your Express server, it doesn't go directly to your route handler. Instead, it travels through a pipeline of middleware functions — one by one, in the order you defined them. Each middleware can inspect, modify, or reject the request before passing it along. Here's what that journey looks like:

📥
Incoming Request

Client sends an HTTP request (e.g., GET /api/users). It carries a method, URL, headers, and possibly a body. Express receives it and starts the middleware chain.

passes to first middleware
1
Logger

Records the request method, URL, and timestamp. Doesn't change anything — just observes. Then calls next() to pass the request forward.

next()
2
JSON Parser

Reads the raw request body, parses it as JSON, and attaches the result to req.body. Without this, req.body would be undefined. Calls next().

next()
3
Auth Check

Verifies the user's token or session. If valid, it may attach req.user and call next(). If invalid, it stops the chain and sends a 401 response — the request never reaches the route handler!

next()
🎯
Route Handler

Your actual business logic runs here. It has access to everything the middleware attached: req.body (from JSON parser), req.user (from auth), and the full req/res objects.

sends response
📤
Response

The handler calls res.json(), res.send(), or res.render() to send data back to the client. The request-response cycle ends here.

📐 The Middleware Signature

Every middleware function receives exactly 3 parameters (or 4 for error middleware). This is the universal pattern — memorize it:

JavaScript
// Every middleware looks like this:
function myMiddleware(req, res, next) {
    // req  → the incoming request object (URL, headers, body...)
    // res  → the response object (methods to send data back)
    // next → function to call when you're done (pass to next middleware)

    // Do your work here...
    console.log('I am middleware!');

    next(); // Pass control to the next middleware in the chain
}

🏭 Think of It Like a Factory Conveyor Belt

Each middleware is a station on the belt. The request (product) moves through each station in order. Each station can inspect it (logger), add to it (JSON parser adds req.body), reject it (auth sends 401), or just let it pass. If any station calls next(), the product moves to the next station. If not — it stops there forever (the request hangs).

⚡ The next() Function: Critical!

You MUST call next() to pass control to the next middleware. If you forget, the request hangs forever!

✅ Correct: Calls next()

JavaScript
app.use((req, res, next) => {
    console.log('Request received');
    next(); // ✅ Passes control!
});

❌ Wrong: Forgets next()

JavaScript
app.use((req, res, next) => {
    console.log('Request received');
    // ❌ No next()! Request hangs!
});

Exception: If you send a response (res.send(), res.json()), you don't need next() because the request-response cycle ends there.

🛠️ Middleware Examples

Let's look at the most common middleware patterns you'll use in real applications. Notice how each one follows the same (req, res, next) signature and calls next() to pass control forward:

📝

1. Logger Middleware

Application-Level

Logs every request that comes to your server:

JavaScript
app.use((req, res, next) => {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] ${req.method} ${req.url}`);
    next(); // ✅ Pass to next middleware
});
🔐

2. Authentication Middleware

Route-Specific

Checks if user is authenticated before allowing access:

JavaScript
function requireAuth(req, res, next) {
    const token = req.headers.authorization;
    if (!token) {
        return res.status(401).json({ error: 'Unauthorized' });
    }
    req.user = { id: 123, name: 'Alice' };
    next(); // ✅ Authenticated
}

// Protected route
app.get('/dashboard', requireAuth, (req, res) => {
    res.json({ message: 'Welcome!', user: req.user });
});
⏱️

3. Request Timing

Performance

Measures how long each request takes:

JavaScript
app.use((req, res, next) => {
    const start = Date.now();
    res.on('finish', () => {
        const duration = Date.now() - start;
        console.log(`${req.method} ${req.url} - ${duration}ms`);
    });
    next();
});
⚠️

4. Error Handler (4 parameters!)

Error Handling

Error middleware has 4 parameters including err:

JavaScript
app.use((err, req, res, next) => {
    console.error('Error:', err.message);
    res.status(500).json({ error: err.message });
});

// Trigger error in a route
app.get('/fail', (req, res, next) => {
    next(new Error('Something went wrong!'));
});

🎨 Types of Middleware

Express middleware comes in six categories. Understanding the difference helps you choose the right approach for each situation — from global logging to specific route protection:

🌍

Application-Level

Runs for all routes via app.use()

JavaScript
app.use(express.json());
app.use(logger);
app.use(cors());
🛤️

Router-Level

Bound to specific router instances

JavaScript
const router = express.Router();
router.use(auth);
router.get('/admin', handler);
🎯

Route-Specific

Runs only for specific routes

JavaScript
app.get('/admin', auth, (req, res) => {
    res.send('Admin page');
});
📦

Built-in

Express's built-in middleware

JavaScript
express.json()
express.text()
express.urlencoded()
express.static()
🌐

Third-Party

npm packages you install

JavaScript
const cors = require('cors');
const helmet = require('helmet');
app.use(cors());
app.use(helmet());
⚠️

Error-Handling

Has 4 parameters (err, req, res, next)

JavaScript
app.use((err, req, res, next) => {
    res.status(500).json({
        error: err.message
    });
});

⚠️ Order Matters!

Middleware executes in the order you define it. Always put express.json() BEFORE routes that need req.body. Error middleware must be LAST!

🎯 Complete Middleware Example
JavaScript
const express = require('express');
const app = express();

// 1️⃣ Application-level: runs for ALL requests
app.use((req, res, next) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
    next();
});

// 2️⃣ Built-in middleware
app.use(express.json());

// 3️⃣ Custom authentication middleware
function requireAuth(req, res, next) {
    const token = req.headers.authorization;
    if (!token) return res.status(401).json({ error: 'Unauthorized' });
    req.user = { id: 123, name: 'Alice' };
    next();
}

// 4️⃣ Public route (no middleware)
app.get('/', (req, res) => { res.send('Welcome!'); });

// 5️⃣ Protected route
app.get('/dashboard', requireAuth, (req, res) => {
    res.json({ message: 'Dashboard', user: req.user });
});

// 6️⃣ Multiple middleware in sequence
app.post('/admin', requireAuth, checkAdmin, (req, res) => {
    res.send('Admin panel');
});

function checkAdmin(req, res, next) {
    if (req.user.role !== 'admin')
        return res.status(403).json({ error: 'Forbidden' });
    next();
}

// 7️⃣ Error-handling middleware (MUST be last!)
app.use((err, req, res, next) => {
    console.error('Error:', err.message);
    res.status(500).json({ error: 'Internal Server Error' });
});

app.listen(3000);

🎓 Key Takeaways

  • Middleware are functions that process requests in a queue/pipeline
  • Always call next() unless you send a response
  • Order matters! Middleware runs top-to-bottom
  • Use middleware for logging, authentication, parsing, error handling
  • Error middleware has 4 parameters: (err, req, res, next)
  • Middleware can modify req and res for later use

🔄 Complete REST API (CRUD)

REST (Representational State Transfer) is an architectural style for designing APIs. It uses HTTP methods to perform CRUD operations on resources.

🎯 What Makes an API "RESTful"?

REST follows simple but important rules: 1) Resources are identified by URLs (/users, /products/42). 2) HTTP methods define what you do to a resource. 3) Each request is stateless — the server doesn't remember previous requests. 4) Responses use standard HTTP status codes. These conventions make APIs predictable: if you know REST, you can use any REST API without reading docs first!

📋 CRUD = Create, Read, Update, Delete

CRUD stands for the four basic operations you can perform on any data. REST maps each operation to a specific HTTP method. This mapping is a universal convention — every developer worldwide follows it:

OperationHTTP MethodRouteExample
CreatePOST/api/usersCreate a user
Read allGET/api/usersList all users
Read oneGET/api/users/:idGet user by ID
UpdatePUT/api/users/:idReplace a user
DeleteDELETE/api/users/:idRemove a user

🎯 Complete Todo API

Let's build a complete working API step by step. This Todo API demonstrates all five CRUD operations. Notice the patterns: we use an in-memory array (no database yet), auto-increment IDs, proper status codes, and input validation. Study each route carefully — you'll reuse these exact patterns in every API you build!

JavaScript
const express = require('express');
const app = express();
app.use(express.json());

let todos = [
    { id: 1, title: 'Learn Node.js', completed: false },
    { id: 2, title: 'Build an API', completed: false }
];
let nextId = 3;

// GET /todos — Get all todos
app.get('/todos', (req, res) => {
    res.json(todos);
});

// GET /todos/:id — Get one todo
app.get('/todos/:id', (req, res) => {
    const todo = todos.find(t => t.id === parseInt(req.params.id));
    if (!todo) return res.status(404).json({ error: 'Todo not found' });
    res.json(todo);
});

// POST /todos — Create a todo
app.post('/todos', (req, res) => {
    const { title } = req.body;
    if (!title) return res.status(400).json({ error: 'Title is required' });
    const newTodo = { id: nextId++, title, completed: false };
    todos.push(newTodo);
    res.status(201).json(newTodo);
});

// PUT /todos/:id — Update a todo
app.put('/todos/:id', (req, res) => {
    const todo = todos.find(t => t.id === parseInt(req.params.id));
    if (!todo) return res.status(404).json({ error: 'Todo not found' });
    const { title, completed } = req.body;
    if (title !== undefined) todo.title = title;
    if (completed !== undefined) todo.completed = completed;
    res.json(todo);
});

// DELETE /todos/:id — Delete a todo
app.delete('/todos/:id', (req, res) => {
    const index = todos.findIndex(t => t.id === parseInt(req.params.id));
    if (index === -1) return res.status(404).json({ error: 'Not found' });
    const deleted = todos.splice(index, 1)[0];
    res.json({ message: 'Deleted', todo: deleted });
});

app.listen(3000, () => console.log('Todo API on http://localhost:3000'));

🔍 Key Patterns to Notice

201 Created for POST (not 200!), 404 Not Found when the resource doesn't exist, 400 Bad Request for missing required data. Also notice: parseInt(req.params.id) — route params are always strings, so you must convert them to numbers for comparisons! These patterns repeat in every REST API.

📖 PUT vs PATCH — What's the Difference?

PUT = full replacement: you send the entire new object. PATCH = partial update: you only send the fields you want to change. In practice, many APIs use PUT for both (like our example above), but knowing the difference matters for API design interviews and documentation!

🚨 Error Handling in Express (6-Part Deep Dive)

Proper error handling is crucial for building robust applications. Without it, your server crashes on unexpected errors, leaving users with no response. Let's go from crashes to production-ready, step by step.

⚠️ Part 1: The Problem — Unhandled Errors Crash Your Server

JavaScript
// ❌ BAD: No error handling — this WILL crash!
app.get('/todos/:id', (req, res) => {
    const todo = todos.find(t => t.id === parseInt(req.params.id));
    // What if todo is undefined? 💥 CRASH!
    res.json({ task: todo.task }); // Cannot read property of undefined
});

// GET /todos/999 → 💥 Server crashes! All users lose connection!

💥 What Happens When It Crashes?

Your entire server stops. Users get no response. You must manually restart. All connections lost. This is unacceptable in production!

✅ Part 2: Solution 1 — Try-Catch (Works, But Repetitive)

JavaScript
// ✅ Server doesn't crash, but... 
app.get('/todos/:id', (req, res) => {
    try {
        const id = parseInt(req.params.id);
        if (isNaN(id)) return res.status(400).json({ error: 'Invalid ID' });
        const todo = todos.find(t => t.id === id);
        if (!todo) return res.status(404).json({ error: 'Not found' });
        res.json(todo);
    } catch (error) {
        res.status(500).json({ error: 'Something went wrong' });
    }
});

app.post('/todos', (req, res) => {
    try { /* same try-catch pattern... */ } catch (error) { /* same... */ }
});

app.delete('/todos/:id', (req, res) => {
    try { /* same try-catch pattern... */ } catch (error) { /* same... */ }
});
// 😩 Repetitive! DRY violation!

🎯 Part 3: Solution 2 — asyncHandler Wrapper (Best Practice ⭐)

Instead of repeating try-catch, create a reusable wrapper function that automatically catches errors. Write error handling ONCE, use it everywhere!

💡 The Core Concept

A Higher-Order Function: takes your route handler, wraps it in try-catch, and returns the wrapped version. Express calls the wrapped function — if it throws, the error is caught automatically!

JavaScript
// The asyncHandler wrapper — write ONCE!
const asyncHandler = (fn) => {
    return async (req, res, next) => {
        try {
            await fn(req, res, next);
        } catch (error) {
            next(error); // Pass to error middleware
        }
    };
};

// Now all routes are clean — no try-catch!
app.get('/todos/:id', asyncHandler(async (req, res) => {
    const id = parseInt(req.params.id);
    const todo = todos.find(t => t.id === id);
    if (!todo) return res.status(404).json({ error: 'Not found' });
    res.json(todo);
}));

app.post('/todos', asyncHandler(async (req, res) => {
    const { task } = req.body;
    if (!task) throw new Error('Task required'); // Caught by wrapper!
    const newTodo = { id: todos.length + 1, task };
    todos.push(newTodo);
    res.json(newTodo);
}));
// 😊 Much cleaner! Error handling in ONE place!

🔍 Error Flow

1️⃣ Error thrown in route → 2️⃣ asyncHandler catches it → 3️⃣ Calls next(error) → 4️⃣ Express error middleware handles it → 5️⃣ Response sent to client → 6️⃣ Server keeps running!

🛡️ Part 4: Error Handling Middleware (Centralized Responses)

Now that asyncHandler catches errors and passes them via next(error), we need error middleware to send proper responses. Handle ALL errors in ONE place!

JavaScript
// 404 handler (no route matched) — before error middleware
app.use((req, res) => {
    res.status(404).json({ error: 'Route not found', path: req.path });
});

// Error handler — MUST be LAST and have 4 parameters!
app.use((err, req, res, next) => {
    const statusCode = err.statusCode || 500;
    console.error(`[${statusCode}] ${err.message}`);

    res.status(statusCode).json({
        success: false,
        error: process.env.NODE_ENV === 'development'
            ? err.message
            : 'An error occurred',
        ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    });
});
🔍 Understanding ...(condition && { property })

This is "Conditional Property Inclusion" using the spread operator:

JavaScript
// true && { stack: 'error' } → returns { stack: 'error' }
// false && { stack: 'error' } → returns false

// ...{ stack: err.stack } → adds stack property
// ...false → spreads nothing (ignored)

const isDev = true;
const obj = { name: 'error', ...(isDev && { details: 'info' }) };
// → { name: 'error', details: 'info' }

const isProd = false;
const obj = { name: 'error', ...(isProd && { details: 'info' }) };
// → { name: 'error' }  — details NOT added

✨ Part 5: Custom Error Classes (Enhancement)

Important: Custom error classes are NOT a catching solution! They help you CREATE better errors with proper status codes. They work best combined with asyncHandler.

⚠️ Common Misconception

asyncHandler = catches errors ✅ | Custom Error Classes = makes errors better ✨ | Together = clean code + meaningful errors! 🚀

JavaScript
// Custom error classes
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;
    }
}

class NotFoundError extends AppError {
    constructor(resource = 'Resource') {
        super(`${resource} not found`, 404);
    }
}

class ValidationError extends AppError {
    constructor(message) { super(message, 400); }
}

// ✅ BEST: Custom Errors + asyncHandler
app.get('/todos/:id', asyncHandler(async (req, res) => {
    const id = parseInt(req.params.id);
    if (isNaN(id)) throw new ValidationError('ID must be a number');
    const todo = todos.find(t => t.id === id);
    if (!todo) throw new NotFoundError('Todo');
    res.json(todo);
}));

🎯 Part 6: Complete Production-Ready Example

📄 Full Production-Ready Error Handling Code
JavaScript
const express = require('express');
const app = express();
app.use(express.json());

// 1️⃣ Custom Error Classes
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;
    }
}
class NotFoundError extends AppError {
    constructor(resource = 'Resource') { super(`${resource} not found`, 404); }
}
class ValidationError extends AppError {
    constructor(message) { super(message, 400); }
}

// 2️⃣ asyncHandler Wrapper
const asyncHandler = (fn) => async (req, res, next) => {
    try { await fn(req, res, next); } catch (error) { next(error); }
};

// 3️⃣ Sample Data
const todos = [
    { id: 1, task: 'Learn Express', completed: false },
    { id: 2, task: 'Build API', completed: true }
];

// 4️⃣ Routes — clean with asyncHandler + Custom Errors
app.get('/todos', asyncHandler(async (req, res) => { res.json(todos); }));

app.get('/todos/:id', asyncHandler(async (req, res) => {
    const id = parseInt(req.params.id);
    if (isNaN(id)) throw new ValidationError('ID must be a valid number');
    const todo = todos.find(t => t.id === id);
    if (!todo) throw new NotFoundError('Todo');
    res.json(todo);
}));

app.post('/todos', asyncHandler(async (req, res) => {
    const { task } = req.body;
    if (!task || task.trim() === '') throw new ValidationError('Task is required');
    if (task.length > 100) throw new ValidationError('Task too long (max 100)');
    const newTodo = { id: todos.length + 1, task: task.trim(), completed: false };
    todos.push(newTodo);
    res.status(201).json(newTodo);
}));

// 5️⃣ 404 Handler
app.use((req, res) => {
    res.status(404).json({ success: false, error: 'Route not found', path: req.path });
});

// 6️⃣ Error Middleware (MUST be LAST!)
app.use((err, req, res, next) => {
    const statusCode = err.statusCode || 500;
    console.error(`[${statusCode}] ${err.message}`);
    res.status(statusCode).json({
        success: false, error: err.message || 'Internal Server Error'
    });
});

app.listen(3000, () => console.log('✅ Server running on port 3000'));
// GET  /todos/1    → 200  |  GET /todos/999    → 404
// GET  /todos/abc  → 400  |  POST /todos (empty) → 400
// GET  /invalid    → 404 (Route not found)

🔑 Error Handling Key Takeaways

  • 📌 Part 1: Unhandled errors crash your server — unacceptable!
  • 📌 Part 2: Try-catch works but creates repetitive code
  • 📌 Part 3: asyncHandler wrapper = DRY principle, write once
  • 📌 Part 4: Error middleware catches all errors centrally
  • 📌 Part 5: Custom error classes = proper status codes + messages
  • Best Practice: asyncHandler + Custom Errors + Error Middleware = Production-ready!

✅ Input Validation

Never trust user input! Validation ensures data is correct, complete, and safe before processing.

🔒 The Golden Rule of Backend Development

Never trust data from the client. Even if your frontend validates the data, anyone can bypass it using Postman, curl, or browser dev tools. Backend validation is your last line of defense — without it, attackers can inject malicious data, crash your server, or corrupt your database. Always validate on the server, no exceptions!

🤔 Why Validate Input?

🐛

Prevent Bugs

Invalid data causes crashes and unexpected behavior

💾

Data Integrity

Keep your database clean and consistent

👤

Better UX

Give users clear, actionable error messages

📏

Enforce Rules

Ensure data meets business requirements

🔧 Manual Validation

The simplest approach: check each field yourself with if statements. This gives you full control but becomes repetitive and hard to maintain as your API grows. Notice how we collect all errors into an array and return them together — this gives users all the issues at once instead of one at a time:

JavaScript
app.post('/users', (req, res) => {
    const { name, email, age } = req.body;
    const errors = [];

    if (!name || name.trim() === '') errors.push('Name is required');
    if (!email) errors.push('Email is required');
    else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
        errors.push('Email must be valid');
    if (age !== undefined) {
        const ageNum = parseInt(age);
        if (isNaN(ageNum)) errors.push('Age must be a number');
        else if (ageNum < 0 || ageNum > 150) errors.push('Age: 0-150');
    }

    if (errors.length > 0)
        return res.status(400).json({ success: false, errors });

    res.status(201).json({ success: true, data: { name, email, age } });
});

⚠️ Problem: Manual validation becomes messy and repetitive quickly. For complex apps, use a validation library!

📦 express-validator Library

express-validator is the most popular validation library for Express. It provides a clean, middleware-based API.

It works as middleware — you chain validation rules before your route handler. Each rule checks one field and adds an error if the check fails. After all rules run, you collect the errors with validationResult(req). This approach is declarative (you describe what you want, not how to check it) and much cleaner than manual validation:

Bash
npm install express-validator
JavaScript
const { body, param, validationResult } = require('express-validator');

app.post('/users',
    // Validation middleware chain
    body('name').trim().notEmpty().withMessage('Name is required')
        .isLength({ min: 2, max: 100 }).withMessage('Name: 2-100 chars'),
    body('email').trim().notEmpty().withMessage('Email is required')
        .isEmail().withMessage('Email must be valid').normalizeEmail(),
    body('age').optional()
        .isInt({ min: 0, max: 150 }).withMessage('Age: 0-150').toInt(),

    // Route handler
    (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty())
            return res.status(400).json({ success: false, errors: errors.array() });

        const { name, email, age } = req.body;
        res.status(201).json({ success: true, data: { name, email, age } });
    }
);

📋 Common Validation Rules

express-validator provides dozens of built-in validators and sanitizers. Here are the ones you'll use most often — validators check the data, and sanitizers (like trim(), normalizeEmail()) clean it up automatically:

notEmpty()Field must not be empty
isEmail()Must be valid email
isInt()Must be integer
isLength()Check string length
isURL()Must be valid URL
isStrongPassword()Check password strength
matches()Match regex pattern
isIn()Value in allowed list
custom()Custom validation logic
optional()Field is optional
trim()Remove whitespace
normalizeEmail()Standardize email
🔥 Advanced Validation: Reusable Middleware & Custom Rules
JavaScript
// validators/userValidators.js — Reusable!
const { body, validationResult } = require('express-validator');

const userValidationRules = () => [
    body('name').trim().notEmpty().isLength({ min: 2, max: 100 }),
    body('email').trim().notEmpty().isEmail().normalizeEmail(),
    body('age').optional().isInt({ min: 0, max: 150 }).toInt()
];

const validate = (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty())
        return res.status(400).json({
            success: false,
            errors: errors.array().map(e => ({ field: e.path, message: e.msg }))
        });
    next();
};

// Usage — super clean!
app.post('/users', userValidationRules(), validate, (req, res) => {
    res.status(201).json({ success: true, data: req.body });
});

// Custom validation functions
const userExists = async (value) => {
    const user = await db.findUserByEmail(value);
    if (user) throw new Error('Email already exists');
    return true;
};

app.post('/register',
    body('email').isEmail().custom(userExists),
    body('password').isLength({ min: 8 })
        .matches(/\d/).withMessage('Must contain a number')
        .matches(/[A-Z]/).withMessage('Must contain uppercase'),
    body('confirmPassword').custom((val, { req }) => {
        if (val !== req.body.password) throw new Error('Passwords don\'t match');
        return true;
    }),
    validate, handler
);

📊 Logging Best Practices

Good logging is essential for debugging, monitoring, and understanding your app. console.log() is fine for learning, but production apps need structured, level-based logging.

💡 Why Logging Matters in Production

Imagine your API crashes at 3 AM. You can't attach a debugger or add console.log() after the fact. Logs are your only evidence. Good logging tells you exactly what happened, when, and why — like a flight recorder for your server. Without it, debugging production issues is like finding a needle in a haystack... blindfolded.

🤔 Why Move Beyond console.log()?

No Log Levels

Can't distinguish info, warnings, errors

No Structure

Hard to parse and search logs

No Persistence

Logs disappear on restart

Production Issues

Can't filter or control in production

📊 Log Levels

Log levels let you categorize messages by severity. In production, you typically only show info and above (hiding debug noise). In development, you show everything. This way, you don't drown in log data but can still find critical issues:

🔴error

Critical issues. Server errors, crashes, failures.

logger.error('Database connection failed');
🟡warn

Potential issues. Not critical but investigate.

logger.warn('API rate limit approaching');
🔵info

Important events: server start, user actions.

logger.info('Server started on port 3000');
🟣debug

Detailed debugging info. Only in development.

logger.debug('Query executed:', query);

📦 Winston: Professional Logging

Winston is Node.js's most popular logging library. It supports multiple transports (outputs) — console, files, HTTP endpoints — so you can send logs wherever you need them. It also supports formats (JSON, colorized, timestamped) and log rotation (automatic file size limits):

Bash
npm install winston
JavaScript
// config/logger.js
const winston = require('winston');
const isDev = process.env.NODE_ENV === 'development';

const logger = winston.createLogger({
    level: isDev ? 'debug' : 'info',
    format: winston.format.combine(
        winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
        winston.format.errors({ stack: true }),
        winston.format.json()
    ),
    defaultMeta: { service: 'api-server' },
    transports: [
        new winston.transports.Console({
            format: isDev
                ? winston.format.combine(winston.format.colorize(), winston.format.simple())
                : winston.format.json()
        }),
        new winston.transports.File({ filename: 'logs/error.log', level: 'error', maxsize: 5242880, maxFiles: 5 }),
        new winston.transports.File({ filename: 'logs/combined.log', maxsize: 5242880, maxFiles: 5 })
    ]
});

module.exports = logger;

// Usage
const logger = require('./config/logger');
logger.info('Server started', { port: 3000, env: process.env.NODE_ENV });
logger.error('DB failed', { error: err.message, stack: err.stack });
logger.warn('High memory usage');
logger.debug('Request payload:', req.body);

📝 What to Log (and What NOT to Log)

✅ DO Log

  • Server start/stop events
  • Incoming requests (method, path, IP)
  • Database operations
  • External API calls
  • Auth attempts (success/failure)
  • Errors with context
  • Performance metrics

❌ DON'T Log

  • 🔒 Passwords (EVER!)
  • 🔑 API keys or tokens
  • 💳 Credit card numbers
  • 🔐 Session IDs
  • 📱 Personal identifiable info (PII)
  • 🗝️ Encryption keys

⚠️ Security Warning: Logging sensitive data can lead to security breaches! Attackers often gain access through log files. Always sanitize data before logging.

📤 API Response Patterns

Consistent API responses make your API easier to use and maintain. Clients know exactly what to expect.

💡 Why Does This Matter?

Imagine calling an API where success returns { user: {...} }, errors return { msg: "fail" }, and lists return [...]. Every endpoint has a different shape — your frontend code becomes a mess of special cases! A consistent format means your client-side error handling works the same way everywhere: just check response.success, grab response.data or response.error, done.

🤔 Why Consistent Responses Matter

🎯

Predictable

Clients know what to expect every time

🐛

Fewer Bugs

Consistent format reduces parsing errors

📚

Self-Documenting

Clear response structure is easy to understand

🔧

Easy to Maintain

Standardized responses simplify refactoring

✅ Standard Response Format

Choose one format and use it everywhere. The most common pattern is a wrapper object with success (boolean), data (the actual content), and error (the message). Here's the format used by most professional APIs:

JavaScript
// Success response
{ "success": true, "data": { /* ... */ }, "message": "Optional message" }

// Error response
{ "success": false, "error": "Error message", "errors": [ /* details */ ] }

// Paginated response
{
    "success": true,
    "data": [ /* items */ ],
    "pagination": { "page": 1, "limit": 10, "total": 100, "totalPages": 10 }
}

🔧 Response Helper Class

Instead of manually typing the same { success: true, data: ... } structure in every route, create a helper class. This ensures consistency and reduces typos. If you ever need to change your response format, you change it in one place instead of 50 routes:

JavaScript
class ApiResponse {
    static success(res, data, message = null, statusCode = 200) {
        return res.status(statusCode).json({
            success: true, data, ...(message && { message })
        });
    }
    static error(res, message, statusCode = 500, errors = null) {
        return res.status(statusCode).json({
            success: false, error: message, ...(errors && { errors })
        });
    }
    static paginated(res, data, page, limit, total) {
        return res.status(200).json({
            success: true, data,
            pagination: {
                page: parseInt(page), limit: parseInt(limit), total,
                totalPages: Math.ceil(total / limit),
                hasNext: page * limit < total, hasPrev: page > 1
            }
        });
    }
    static created(res, data, msg = 'Created') { return this.success(res, data, msg, 201); }
    static noContent(res) { return res.status(204).send(); }
    static badRequest(res, msg, errors = null) { return this.error(res, msg, 400, errors); }
    static unauthorized(res, msg = 'Unauthorized') { return this.error(res, msg, 401); }
    static notFound(res, msg = 'Not found') { return this.error(res, msg, 404); }
}

// Usage
app.get('/users', async (req, res) => {
    const users = await db.getUsers();
    return ApiResponse.success(res, users);
});

app.post('/users', validate, async (req, res) => {
    const user = await db.createUser(req.body);
    return ApiResponse.created(res, user);
});

📊 HTTP Status Codes Reference

Status codes are not arbitrary — they carry meaning. Clients, browsers, and tools (like Postman) use them to determine what happened. Using the right code makes your API self-documenting. Here are the ones you'll use most:

2xx Success

200 OKRequest successful
201 CreatedResource created
204 No ContentSuccess, no content

4xx Client Errors

400 Bad RequestInvalid request
401 UnauthorizedNot authenticated
403 ForbiddenNo permission
404 Not FoundResource not found
422 UnprocessableValidation failed

5xx Server Errors

500 InternalServer error
503 UnavailableServer down

✅ Best Practices

Building an API that works is one thing. Building an API that's maintainable, secure, and scalable is another. These best practices come from real production experience — follow them from the start and you'll avoid painful refactoring later:

📁

Project Structure

File Tree
my-api/
├── server.js
├── routes/
│   └── users.js
├── middleware/
│   └── auth.js
├── controllers/
│   └── userController.js
├── validators/
│   └── userValidators.js
├── config/
│   └── logger.js
├── utils/
│   └── response.js
├── .env
├── .gitignore
└── package.json
🔐

Environment Variables

Bash
# .env file
PORT=3000
DB_URL=mongodb://localhost
JWT_SECRET=my-secret-key
NODE_ENV=development

# .gitignore
node_modules/
.env
logs/

Use async/await

JavaScript
// Always use asyncHandler
app.get('/users', asyncHandler(
    async (req, res) => {
        const users = await db.getUsers();
        ApiResponse.success(res, users);
    }
));
🔄

Consistent Responses

JavaScript
// ✅ Always same shape
res.json({
    success: true,
    data: users
});
res.status(400).json({
    success: false,
    error: 'Invalid input'
});
🎓

You're Now a Full-Stack Developer!

You know JavaScript (browser), Node.js (server), and Express (web framework). You can build complete applications from frontend to backend! The journey doesn't stop here — keep building, keep learning! 🚀

📝 Summary

⚡ Express

Web framework. app.get/post/put/delete for clean routing.

🛣️ Routing

Params (:id), query strings, request body, Router for organization.

📦 Body Parsing

Built-in: express.json(), .text(), .urlencoded(), .static().

🔗 Middleware

Pipeline of functions. Logger, auth, CORS. Order matters!

🔄 REST API

CRUD operations mapped to HTTP methods on resource URLs.

🛡️ Error Handling

asyncHandler + custom errors + centralized error middleware.

✅ Validation

express-validator for clean, reusable input validation.

📊 Logging

Winston for structured, level-based, production-ready logging.

📤 API Patterns

Consistent response format, status codes, helper classes.

6
Next Module 6 / 7

Module 6: Authentication & Security

Sessions, JWT, OAuth 2.0, password hashing, security headers, and common vulnerabilities.

sessions JWT OAuth 2.0 bcrypt CORS helmet