04-expressjs.md

Module 04: Express.js & REST APIs

Goal: Master Express.js and learn how to build production-ready REST APIs. Time: 2-3 days of focused study Prerequisites: Module 01-03


Table of Contents

  1. HTTP Fundamentals
  2. What is Express.js?
  3. Routing
  4. Middleware — The Core Concept
  5. Request & Response Objects
  6. Error Handling
  7. REST API Design
  8. Input Validation
  9. File Uploads
  10. Serving Static Files
  11. CORS
  12. Rate Limiting
  13. Project Structure
  14. Real-World API Example
  15. Interview Questions

1. HTTP Fundamentals

Before Express, you need to understand HTTP.

HTTP Methods

Method │ Purpose │ Idempotent │ Safe │ Request Body ────────┼──────────────────────┼────────────┼──────┼───────────── GET │ Retrieve resource │ ✅ Yes │ ✅ │ No POST │ Create resource │ ❌ No │ ❌ │ Yes PUT │ Replace resource │ ✅ Yes │ ❌ │ Yes PATCH │ Partial update │ ❌ No │ ❌ │ Yes DELETE │ Delete resource │ ✅ Yes │ ❌ │ Optional HEAD │ Like GET, no body │ ✅ Yes │ ✅ │ No OPTIONS │ Supported methods │ ✅ Yes │ ✅ │ No Idempotent = same request multiple times has same effect Safe = doesn't modify the server state

HTTP Status Codes

1xx — Informational 100 Continue 2xx — Success 200 OK — Standard success 201 Created — Resource created (after POST) 204 No Content — Success, no body (after DELETE) 3xx — Redirection 301 Moved Permanently — URL changed permanently 302 Found — Temporary redirect 304 Not Modified — Use cached version 4xx — Client Error 400 Bad Request — Invalid input 401 Unauthorized — Not authenticated 403 Forbidden — Authenticated but not authorized 404 Not Found — Resource doesn't exist 405 Method Not Allowed — Wrong HTTP method 409 Conflict — Resource already exists 422 Unprocessable Entity — Validation failed 429 Too Many Requests — Rate limited 5xx — Server Error 500 Internal Server Error — Generic server error 502 Bad Gateway — Upstream server error 503 Service Unavailable — Server temporarily down 504 Gateway Timeout — Upstream timeout

HTTP Headers

Request Headers: Content-Type: application/json — Body format Authorization: Bearer <token> — Authentication Accept: application/json — Expected response format User-Agent: Mozilla/5.0... — Client info Cookie: sessionId=abc123 — Cookies Response Headers: Content-Type: application/json — Body format Set-Cookie: sessionId=abc123 — Set browser cookie Cache-Control: max-age=3600 — Caching instructions Access-Control-Allow-Origin: * — CORS X-RateLimit-Remaining: 99 — Custom headers (X- prefix)

2. What is Express.js?

Express.js is a minimal, flexible web framework for Node.js. It provides:

  • Routing (URL → handler mapping)
  • Middleware system (request/response pipeline)
  • Template engine support
  • Static file serving
const express = require('express'); const app = express(); // Built-in middleware for parsing JSON bodies app.use(express.json()); // Simple route app.get('/', (req, res) => { res.json({ message: 'Hello, World!' }); }); // Start server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });

3. Routing

Basic Routing

const express = require('express'); const app = express(); // Route = HTTP method + URL path + handler function app.get('/users', (req, res) => { res.json({ users: [] }); // GET /users }); app.post('/users', (req, res) => { res.status(201).json({ user: req.body }); // POST /users }); app.put('/users/:id', (req, res) => { res.json({ updated: req.params.id }); // PUT /users/123 }); app.patch('/users/:id', (req, res) => { res.json({ patched: req.params.id }); // PATCH /users/123 }); app.delete('/users/:id', (req, res) => { res.status(204).send(); // DELETE /users/123 }); // All methods app.all('/secret', (req, res) => { res.send('Any method works here'); });

Route Parameters

// Parameters in URL path (required) app.get('/users/:userId', (req, res) => { console.log(req.params.userId); // "123" for /users/123 res.json({ userId: req.params.userId }); }); // Multiple parameters app.get('/users/:userId/posts/:postId', (req, res) => { console.log(req.params); // { userId: "1", postId: "5" } }); // Optional parameter pattern app.get('/users/:userId/posts/:postId?', (req, res) => { // postId is optional — matches /users/1 AND /users/1/posts/5 }); // Query parameters (from URL: /search?q=hello&page=2) app.get('/search', (req, res) => { console.log(req.query.q); // "hello" console.log(req.query.page); // "2" (always strings!) console.log(req.query); // { q: "hello", page: "2" } });

Router — Modular Routes

// routes/users.js const express = require('express'); const router = express.Router(); // These routes are relative to where the router is mounted router.get('/', (req, res) => { res.json({ users: [] }); }); router.get('/:id', (req, res) => { res.json({ user: { id: req.params.id } }); }); router.post('/', (req, res) => { res.status(201).json({ user: req.body }); }); module.exports = router; // app.js const express = require('express'); const userRoutes = require('./routes/users'); const app = express(); app.use(express.json()); app.use('/api/users', userRoutes); // Mount at /api/users // Now these work: // GET /api/users → router's '/' handler // GET /api/users/123 → router's '/:id' handler // POST /api/users → router's '/' handler

Route Chaining

// Chain methods on the same path app.route('/users') .get((req, res) => { res.json({ users: [] }); }) .post((req, res) => { res.status(201).json({ user: req.body }); }); app.route('/users/:id') .get((req, res) => res.json({ user: {} })) .put((req, res) => res.json({ updated: true })) .delete((req, res) => res.status(204).send());

4. Middleware — The Core Concept

Middleware functions are functions that have access to req, res, and next. They execute in order and form a pipeline.

Request → Middleware 1 → Middleware 2... → Route Handler → Response │ │ ▼ ▼ (can modify (can modify req/res or req/res or end request) end request)

Types of Middleware

const express = require('express'); const app = express(); // 1. APPLICATION-LEVEL MIDDLEWARE — runs on every request app.use((req, res, next) => { console.log(`${req.method} ${req.url}${new Date().toISOString()}`); next(); // MUST call next() to pass to next middleware/route }); // 2. ROUTE-SPECIFIC MIDDLEWARE — runs only on specific routes function authenticate(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'No token provided' }); } // Verify token (we'll learn JWT in Module 05) req.user = { id: 1, name: 'Alice' }; // Attach user to request next(); } app.get('/profile', authenticate, (req, res) => { res.json({ user: req.user }); // Only runs if authenticate calls next() }); // Multiple middleware on one route app.post('/admin/users', authenticate, // Check if logged in requireRole('admin'), // Check if admin validateBody(schema), // Validate input (req, res) => { // Finally, the route handler res.status(201).json({ user: req.body }); } ); // 3. 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 // 4. THIRD-PARTY MIDDLEWARE const cors = require('cors'); const helmet = require('helmet'); const morgan = require('morgan'); const compression = require('compression'); app.use(cors()); // Enable CORS app.use(helmet()); // Security headers app.use(morgan('combined')); // HTTP request logging app.use(compression()); // Gzip compression // 5. ERROR-HANDLING MIDDLEWARE — has 4 parameters (err, req, res, next) app.use((err, req, res, next) => { console.error(err.stack); res.status(err.statusCode || 500).json({ error: err.message || 'Internal Server Error' }); });

How next() Works

// next() passes control to the NEXT middleware/route handler app.use((req, res, next) => { console.log('1. First middleware'); next(); // Go to next middleware console.log('5. Back in first middleware (after everything)'); }); app.use((req, res, next) => { console.log('2. Second middleware'); next(); // Go to route handler console.log('4. Back in second middleware'); }); app.get('/', (req, res) => { console.log('3. Route handler'); res.send('Hello'); }); // Output for GET /: // 1. First middleware // 2. Second middleware // 3. Route handler // 4. Back in second middleware // 5. Back in first middleware // next('route') — skip remaining middleware on this route app.get('/user/:id', (req, res, next) => { if (req.params.id === '0') { next('route'); // Skip to next app.get('/user/:id') handler } else { next(); // Continue to the next middleware in this chain } }, (req, res) => { res.send('Regular user'); } ); app.get('/user/:id', (req, res) => { res.send('Special user (id: 0)'); }); // next(error) — jump to error handling middleware app.get('/risky', (req, res, next) => { try { throw new Error('Something broke!'); } catch (err) { next(err); // Skips all remaining middleware, goes to error handler } });

Building Real Middleware

// 1. Request Timer function requestTimer(req, res, next) { const start = process.hrtime.bigint(); res.on('finish', () => { const end = process.hrtime.bigint(); const durationMs = Number(end - start) / 1e6; console.log(`${req.method} ${req.url}${res.statusCode}${durationMs.toFixed(2)}ms`); }); next(); } // 2. Request ID (for tracking in logs) const crypto = require('crypto'); function requestId(req, res, next) { req.id = req.headers['x-request-id'] || crypto.randomUUID(); res.setHeader('X-Request-ID', req.id); next(); } // 3. API Key Authentication function apiKeyAuth(req, res, next) { const apiKey = req.headers['x-api-key']; if (!apiKey) { return res.status(401).json({ error: 'API key required' }); } if (apiKey !== process.env.API_KEY) { return res.status(403).json({ error: 'Invalid API key' }); } next(); } // 4. Role-based access control (factory pattern!) function requireRole(...roles) { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Authentication required' }); } if (!roles.includes(req.user.role)) { return res.status(403).json({ error: 'Insufficient permissions' }); } next(); }; } // Usage: app.delete('/users/:id', authenticate, requireRole('admin'), deleteUser);

5. Request & Response Objects

Request Object (req)

app.post('/api/users', (req, res) => { // ---- URL INFORMATION ---- req.url; // '/api/users?sort=name' req.originalUrl; // '/api/users?sort=name' (preserves original) req.path; // '/api/users' req.method; // 'POST' req.protocol; // 'http' or 'https' req.hostname; // 'localhost' req.ip; // '127.0.0.1' req.baseUrl; // '/api' (if using router mounted at /api) // ---- PARAMETERS ---- req.params; // Route params: { id: '123' } req.query; // Query string: { sort: 'name', limit: '10' } req.body; // Parsed body (needs express.json() middleware) // ---- HEADERS ---- req.headers; // All headers (lowercase keys) req.headers['content-type']; // 'application/json' req.get('Content-Type'); // Same thing (case-insensitive) req.headers.authorization; // 'Bearer token123' // ---- COOKIES (needs cookie-parser middleware) ---- req.cookies; // { sessionId: 'abc123' } req.signedCookies; // { authToken: 'xyz' } // ---- CHECKS ---- req.is('json'); // Check Content-Type: 'json' or false req.accepts('json'); // Check Accept header req.xhr; // Was it an AJAX request? req.secure; // Is HTTPS? req.fresh; // Is the response still "fresh" (caching)? });

Response Object (res)

app.get('/api/demo', (req, res) => { // ---- SENDING RESPONSES ---- res.send('Hello'); // Send string (auto Content-Type) res.json({ hello: 'world' }); // Send JSON (sets Content-Type) res.status(201).json({ id: 1 }); // Set status + JSON res.sendStatus(204); // Send just status code res.end(); // End response without data // ---- HEADERS ---- res.set('X-Custom', 'value'); // Set header res.set({ // Set multiple headers 'X-Custom': 'value', 'Cache-Control': 'no-store' }); res.get('Content-Type'); // Get response header // ---- COOKIES ---- res.cookie('sessionId', 'abc123', { httpOnly: true, // Can't be accessed by JavaScript secure: true, // Only sent over HTTPS sameSite: 'strict', // CSRF protection maxAge: 3600000 // 1 hour in milliseconds }); res.clearCookie('sessionId'); // ---- REDIRECTS ---- res.redirect('/login'); // 302 redirect (temporary) res.redirect(301, '/new-url'); // 301 redirect (permanent) // ---- FILES ---- res.sendFile('/absolute/path/to/file.pdf'); res.download('/path/to/file.pdf', 'report.pdf'); // Force download // ---- STREAMING ---- res.type('text/event-stream'); // For SSE (Server-Sent Events) res.write('data: hello\n\n'); // Write chunk // Don't call res.end() until you're done streaming });

6. Error Handling

The Async Handler Pattern

// Problem: Express doesn't catch errors in async route handlers! app.get('/users', async (req, res) => { const users = await User.find(); // If this throws, Express WON'T catch it! res.json(users); }); // Solution 1: try/catch in every route (tedious) app.get('/users', async (req, res, next) => { try { const users = await User.find(); res.json(users); } catch (err) { next(err); // Pass to error handler } }); // Solution 2: asyncHandler wrapper (recommended) const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; app.get('/users', asyncHandler(async (req, res) => { const users = await User.find(); res.json(users); })); // Now if User.find() throws, the error goes to the error handler automatically! // Note: Express 5 (currently in beta) handles this natively!

Custom Error Classes

// errors.js class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.isOperational = true; Error.captureStackTrace(this, this.constructor); } } class NotFoundError extends AppError { constructor(resource = 'Resource') { super(`${resource} not found`, 404); } } class ValidationError extends AppError { constructor(message, errors = []) { super(message, 400); this.errors = errors; } } class UnauthorizedError extends AppError { constructor(message = 'Authentication required') { super(message, 401); } } class ForbiddenError extends AppError { constructor(message = 'Access denied') { super(message, 403); } } module.exports = { AppError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError };

Centralized Error Handler

// This MUST be the LAST app.use() — after all routes app.use((err, req, res, next) => { // Log the error console.error(`[${req.id}] Error:`, err); // Operational errors (expected — bad input, not found, etc.) if (err.isOperational) { return res.status(err.statusCode).json({ status: 'error', message: err.message, ...(err.errors && { errors: err.errors }), // Validation errors }); } // Programming errors (unexpected — bugs, crashes) // Don't leak details to client res.status(500).json({ status: 'error', message: process.env.NODE_ENV === 'production' ? 'Something went wrong' : err.message, }); }); // Usage in routes: app.get('/users/:id', asyncHandler(async (req, res) => { const user = await User.findById(req.params.id); if (!user) throw new NotFoundError('User'); res.json(user); }));

7. REST API Design

Resource-Based URLs

Good (noun-based, resource-centric): GET /api/users — List all users POST /api/users — Create a user GET /api/users/123 — Get user 123 PUT /api/users/123 — Replace user 123 PATCH /api/users/123 — Update user 123 DELETE /api/users/123 — Delete user 123 GET /api/users/123/posts — Get posts by user 123 POST /api/users/123/posts — Create a post for user 123 Bad (verb-based, action-centric): GET /api/getUsers POST /api/createUser GET /api/getUserById?id=123 POST /api/deleteUser

Pagination, Filtering, Sorting

// GET /api/users?page=2&limit=20&sort=-createdAt&role=admin&search=alice app.get('/api/users', asyncHandler(async (req, res) => { const { page = 1, limit = 20, sort = '-createdAt', // - prefix = descending role, search, } = req.query; // Build filter const filter = {}; if (role) filter.role = role; if (search) filter.name = { $regex: search, $options: 'i' }; // Parse sort const sortField = sort.startsWith('-') ? sort.slice(1) : sort; const sortOrder = sort.startsWith('-') ? -1 : 1; // Query with pagination const skip = (parseInt(page) - 1) * parseInt(limit); const [users, total] = await Promise.all([ User.find(filter) .sort({ [sortField]: sortOrder }) .skip(skip) .limit(parseInt(limit)), User.countDocuments(filter) ]); res.json({ data: users, pagination: { page: parseInt(page), limit: parseInt(limit), total, pages: Math.ceil(total / parseInt(limit)), hasNext: skip + users.length < total, hasPrev: parseInt(page) > 1, } }); }));

Standard API Response Format

// Consistent response structure // Success response { "status": "success", "data": { ... }, "pagination": { ... } // Only for list endpoints } // Error response { "status": "error", "message": "User not found", "code": "NOT_FOUND", "errors": [ // For validation errors { "field": "email", "message": "Invalid email format" } ] } // Helper functions function sendSuccess(res, data, statusCode = 200) { res.status(statusCode).json({ status: 'success', data, }); } function sendPaginated(res, data, pagination) { res.json({ status: 'success', data, pagination, }); }

8. Input Validation

Using Joi (Popular Validation Library)

// npm install joi const Joi = require('joi'); // Define schemas const schemas = { createUser: Joi.object({ name: Joi.string().min(2).max(50).required(), email: Joi.string().email().required(), password: Joi.string().min(8).max(128) .pattern(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)/) .message('Password must have uppercase, lowercase, and number') .required(), age: Joi.number().integer().min(13).max(120).optional(), role: Joi.string().valid('user', 'admin').default('user'), }), updateUser: Joi.object({ name: Joi.string().min(2).max(50), email: Joi.string().email(), age: Joi.number().integer().min(13).max(120), }).min(1), // At least one field required for PATCH queryParams: Joi.object({ page: Joi.number().integer().min(1).default(1), limit: Joi.number().integer().min(1).max(100).default(20), sort: Joi.string().valid('name', '-name', 'createdAt', '-createdAt'), search: Joi.string().max(100), }), }; // Validation middleware factory function validate(schema, source = 'body') { return (req, res, next) => { const { error, value } = schema.validate(req[source], { abortEarly: false, // Report all errors, not just first stripUnknown: true, // Remove fields not in schema }); if (error) { const errors = error.details.map(detail => ({ field: detail.path.join('.'), message: detail.message, })); return res.status(400).json({ status: 'error', message: 'Validation failed', errors, }); } req[source] = value; // Replace with validated/sanitized data next(); }; } // Usage: app.post('/api/users', validate(schemas.createUser), asyncHandler(async (req, res) => { // req.body is now validated and sanitized const user = await User.create(req.body); res.status(201).json({ data: user }); }) ); app.get('/api/users', validate(schemas.queryParams, 'query'), asyncHandler(async (req, res) => { // req.query is validated with defaults applied const { page, limit, sort, search } = req.query; // ... }) );

9. File Uploads

// npm install multer const multer = require('multer'); const path = require('path'); // Configure storage const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, 'uploads/'); // Directory must exist }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, uniqueSuffix + path.extname(file.originalname)); } }); // File filter const fileFilter = (req, file, cb) => { const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Only JPEG, PNG, and WebP images are allowed'), false); } }; const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024, // 5MB max } }); // Single file upload app.post('/api/avatar', upload.single('avatar'), (req, res) => { // req.file contains the uploaded file info console.log(req.file); // { fieldname, originalname, filename, mimetype, size, path, ... } res.json({ url: `/uploads/${req.file.filename}` }); }); // Multiple files app.post('/api/gallery', upload.array('photos', 10), (req, res) => { // req.files is an array of file objects const urls = req.files.map(f => `/uploads/${f.filename}`); res.json({ urls }); }); // Multiple fields app.post('/api/product', upload.fields([ { name: 'thumbnail', maxCount: 1 }, { name: 'gallery', maxCount: 5 } ]), (req, res) => { console.log(req.files.thumbnail); // Array with 1 file console.log(req.files.gallery); // Array with up to 5 files } );

10. Serving Static Files

const express = require('express'); const path = require('path'); const app = express(); // Serve files from 'public' directory app.use(express.static('public')); // Now: http://localhost:3000/css/style.css serves public/css/style.css // With URL prefix app.use('/static', express.static('public')); // Now: http://localhost:3000/static/css/style.css // With absolute path (recommended) app.use('/static', express.static(path.join(__dirname, 'public'))); // Multiple static directories app.use(express.static('public')); app.use(express.static('uploads')); // Express searches directories in order // With caching (production) app.use(express.static('public', { maxAge: '1d', // Cache for 1 day etag: true, // Enable ETag lastModified: true, // Enable Last-Modified header index: 'index.html', // Default file for directories }));

11. CORS

// Cross-Origin Resource Sharing // Browser security feature that blocks requests from different origins // npm install cors const cors = require('cors'); // Allow all origins (development only!) app.use(cors()); // Allow specific origins app.use(cors({ origin: ['http://localhost:3000', 'https://myapp.com'], methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, // Allow cookies maxAge: 86400, // Preflight cache (seconds) })); // Dynamic origin (e.g., from database) app.use(cors({ origin: (origin, callback) => { const allowedOrigins = ['http://localhost:3000', 'https://myapp.com']; if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } } })); // Per-route CORS app.get('/api/public', cors(), (req, res) => { res.json({ public: true }); });

12. Rate Limiting

// npm install express-rate-limit const rateLimit = require('express-rate-limit'); // Global rate limiter const globalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window per IP message: { status: 'error', message: 'Too many requests, please try again later.', }, standardHeaders: true, // Return rate limit info in headers legacyHeaders: false, // Disable X-RateLimit-* headers }); app.use('/api/', globalLimiter); // Stricter limiter for auth endpoints const authLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // 5 attempts per hour message: { error: 'Too many login attempts. Try again in 1 hour.' }, }); app.post('/api/auth/login', authLimiter, loginHandler); app.post('/api/auth/register', authLimiter, registerHandler);

13. Project Structure

my-api/ ├── src/ │ ├── index.js # Entry point — creates and starts server │ ├── app.js # Express app setup (middleware, routes) │ ├── config/ │ │ ├── index.js # Configuration (env vars, defaults) │ │ └── database.js # Database connection │ ├── routes/ │ │ ├── index.js # Route aggregator │ │ ├── auth.routes.js # Auth routes │ │ └── user.routes.js # User routes │ ├── controllers/ │ │ ├── auth.controller.js │ │ └── user.controller.js │ ├── services/ │ │ ├── auth.service.js # Business logic │ │ └── user.service.js │ ├── models/ │ │ └── user.model.js # Database models │ ├── middleware/ │ │ ├── auth.js # Authentication middleware │ │ ├── validate.js # Validation middleware │ │ ├── errorHandler.js # Error handling middleware │ │ └── rateLimiter.js │ ├── utils/ │ │ ├── asyncHandler.js │ │ ├── logger.js │ │ └── errors.js # Custom error classes │ └── validators/ │ ├── auth.validator.js # Joi schemas for auth │ └── user.validator.js ├── tests/ │ ├── unit/ │ └── integration/ ├── .env # Environment variables (not in git!) ├── .env.example # Template for .env ├── .gitignore ├── package.json └── README.md

Example: How Files Connect

// src/index.js const app = require('./app'); const config = require('./config'); app.listen(config.port, () => { console.log(`Server running on port ${config.port}`); }); // src/app.js const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const routes = require('./routes'); const errorHandler = require('./middleware/errorHandler'); const app = express(); app.use(helmet()); app.use(cors()); app.use(express.json()); app.use('/api', routes); app.use(errorHandler); module.exports = app; // src/routes/index.js const router = require('express').Router(); const userRoutes = require('./user.routes'); const authRoutes = require('./auth.routes'); router.use('/auth', authRoutes); router.use('/users', userRoutes); module.exports = router; // src/routes/user.routes.js const router = require('express').Router(); const { getUsers, getUser, updateUser } = require('../controllers/user.controller'); const auth = require('../middleware/auth'); const { validate } = require('../middleware/validate'); const { updateUserSchema } = require('../validators/user.validator'); router.get('/', auth, getUsers); router.get('/:id', auth, getUser); router.patch('/:id', auth, validate(updateUserSchema), updateUser); module.exports = router; // src/controllers/user.controller.js const userService = require('../services/user.service'); const asyncHandler = require('../utils/asyncHandler'); const getUsers = asyncHandler(async (req, res) => { const users = await userService.findAll(req.query); res.json({ status: 'success', data: users }); }); const getUser = asyncHandler(async (req, res) => { const user = await userService.findById(req.params.id); res.json({ status: 'success', data: user }); }); module.exports = { getUsers, getUser, updateUser }; // src/services/user.service.js const User = require('../models/user.model'); const { NotFoundError } = require('../utils/errors'); class UserService { async findAll(query) { return User.find().sort('-createdAt'); } async findById(id) { const user = await User.findById(id); if (!user) throw new NotFoundError('User'); return user; } } module.exports = new UserService();

14. Real-World API Example

// Complete mini API: Todo List const express = require('express'); const crypto = require('crypto'); const app = express(); app.use(express.json()); // In-memory database (use a real DB in production) let todos = []; // GET /api/todos — List all todos app.get('/api/todos', (req, res) => { const { status, search } = req.query; let filtered = [...todos]; if (status) filtered = filtered.filter(t => t.status === status); if (search) filtered = filtered.filter(t => t.title.toLowerCase().includes(search.toLowerCase()) ); res.json({ status: 'success', data: filtered, count: filtered.length }); }); // GET /api/todos/:id — Get single todo app.get('/api/todos/:id', (req, res) => { const todo = todos.find(t => t.id === req.params.id); if (!todo) { return res.status(404).json({ status: 'error', message: 'Todo not found' }); } res.json({ status: 'success', data: todo }); }); // POST /api/todos — Create todo app.post('/api/todos', (req, res) => { const { title, description } = req.body; if (!title || title.trim().length === 0) { return res.status(400).json({ status: 'error', message: 'Title is required', }); } const todo = { id: crypto.randomUUID(), title: title.trim(), description: description?.trim() || '', status: 'pending', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; todos.push(todo); res.status(201).json({ status: 'success', data: todo }); }); // PATCH /api/todos/:id — Update todo app.patch('/api/todos/:id', (req, res) => { const index = todos.findIndex(t => t.id === req.params.id); if (index === -1) { return res.status(404).json({ status: 'error', message: 'Todo not found' }); } const { title, description, status } = req.body; if (status && !['pending', 'in-progress', 'done'].includes(status)) { return res.status(400).json({ status: 'error', message: 'Status must be: pending, in-progress, or done', }); } todos[index] = { ...todos[index], ...(title && { title: title.trim() }), ...(description !== undefined && { description: description.trim() }), ...(status && { status }), updatedAt: new Date().toISOString(), }; res.json({ status: 'success', data: todos[index] }); }); // DELETE /api/todos/:id — Delete todo app.delete('/api/todos/:id', (req, res) => { const index = todos.findIndex(t => t.id === req.params.id); if (index === -1) { return res.status(404).json({ status: 'error', message: 'Todo not found' }); } todos.splice(index, 1); res.status(204).send(); }); // Error handler app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ status: 'error', message: 'Internal server error' }); }); app.listen(3000, () => console.log('Todo API running on :3000'));

15. Interview Questions

Q1: What is middleware in Express? Explain the middleware chain.

Answer: Middleware functions are functions that have access to req, res, and next. They form a pipeline — each request passes through them in order. Each middleware can: modify req/res, end the request-response cycle, or call next() to pass control to the next middleware. They're used for logging, authentication, parsing, error handling, etc.

Q2: How does Express handle errors?

Answer: Express has a special error-handling middleware identified by 4 parameters: (err, req, res, next). When you call next(err), Express skips all remaining non-error middleware and goes directly to the error handler. For async routes, you need to either wrap in try/catch and call next(err), or use an asyncHandler wrapper. Express 5 will handle async errors natively.

Q3: What's the difference between app.use() and app.get()?

Answer: app.use() matches ANY HTTP method and uses prefix matching (e.g., /api matches /api/users). app.get() only matches GET requests and uses exact path matching. app.use() is for middleware that should run on all routes; app.get() is for specific route handlers.

Q4: How would you structure a large Express application?

Answer: Use separation of concerns: Routes (URL mapping) → Controllers (request handling) → Services (business logic) → Models (data access). Use express.Router() for modular routes. Keep middleware in a dedicated directory. Use environment-based configuration. This makes the code testable, maintainable, and follows the single responsibility principle.

Q5: Explain the difference between PUT and PATCH.

Answer: PUT replaces the entire resource — you must send all fields. PATCH partially updates — you only send the fields you want to change. PUT is idempotent (same request always produces the same result). PATCH is technically not guaranteed to be idempotent but is often implemented as such.

Q6: How do you handle file uploads in Express?

Answer: Express doesn't handle file uploads natively — you need middleware like Multer. Multer parses multipart/form-data requests, stores files (to disk or memory), and adds file info to req.file or req.files. You can configure storage destination, filename, file size limits, and file type filters.


Next Module: 05 - Authentication & Security — JWT, sessions, CSRF, XSS, and protecting your APIs.