Routing and Middleware in Express.js

Module 22: Web Frameworks I (JavaScript) - Monday: Express.js Fundamentals

Understanding Express Routing

Routing in Express refers to how an application's endpoints (URIs) respond to client requests. It defines how different parts of your application handle HTTP methods (GET, POST, PUT, DELETE, etc.).

graph TD A[Client Request] --> B{Router} B -->|GET /users| C[List Users] B -->|POST /users| D[Create User] B -->|GET /users/:id| E[Show User] B -->|PUT /users/:id| F[Update User] B -->|DELETE /users/:id| G[Delete User] style B fill:#f9d71c,stroke:#333,stroke-width:2px

The Mail Sorting Analogy

Express routing is like a mail sorting system in a post office:

  • HTTP Methods (GET, POST, PUT, DELETE) are like different types of mail services (standard mail, express mail, registered mail)
  • Routes (/users, /products, /orders) are like different destinations or departments
  • Route Parameters (/users/:id) are like specific mailboxes within a department
  • Route Handlers are like the mail clerks who process each type of mail for each destination

Just as mail is sorted and directed to its proper destination based on its address and service type, HTTP requests in Express are routed to the appropriate handler based on the URL path and HTTP method.

Basic Routing

Express routes are defined using methods that correspond to HTTP methods. Each route method takes a path and a handler function.

Basic Route Structure


app.METHOD(PATH, HANDLER)
                

Where:

  • app is an instance of Express
  • METHOD is an HTTP request method (lowercase): get, post, put, delete, etc.
  • PATH is a path on the server
  • HANDLER is the function executed when the route is matched

Basic Route Examples


const express = require('express');
const app = express();

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

// POST method route
app.post('/', (req, res) => {
    res.send('Got a POST request');
});

// PUT method route
app.put('/user/:id', (req, res) => {
    res.send(`Got a PUT request at /user/${req.params.id}`);
});

// DELETE method route
app.delete('/user/:id', (req, res) => {
    res.send(`Got a DELETE request at /user/${req.params.id}`);
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});
                

Each route handles a specific HTTP method and URL path. The handler functions take request and response objects as parameters.

Route Parameters

Route parameters are named URL segments used to capture values at specific positions in the URL. The captured values are stored in the req.params object.

Route Parameters Example


// Route with one parameter
app.get('/users/:userId', (req, res) => {
    const userId = req.params.userId;
    res.send(`User ID: ${userId}`);
});

// Route with multiple parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
    const userId = req.params.userId;
    const postId = req.params.postId;
    res.send(`User ID: ${userId}, Post ID: ${postId}`);
});
                

When a client makes a request to /users/123, the userId parameter will be 123. Similarly, a request to /users/123/posts/456 will set userId to 123 and postId to 456.

Real-world Example: Product API

Consider an e-commerce API where you need to retrieve product information:


// Get all products
app.get('/api/products', (req, res) => {
    // Fetch all products from database
    const products = fetchAllProducts();
    res.json(products);
});

// Get a specific product
app.get('/api/products/:productId', (req, res) => {
    const productId = req.params.productId;
    const product = findProductById(productId);
    
    if (!product) {
        return res.status(404).json({ error: 'Product not found' });
    }
    
    res.json(product);
});

// Get all reviews for a product
app.get('/api/products/:productId/reviews', (req, res) => {
    const productId = req.params.productId;
    const reviews = findReviewsByProductId(productId);
    
    res.json(reviews);
});

// Get a specific review for a product
app.get('/api/products/:productId/reviews/:reviewId', (req, res) => {
    const { productId, reviewId } = req.params;
    const review = findReviewByProductIdAndReviewId(productId, reviewId);
    
    if (!review) {
        return res.status(404).json({ error: 'Review not found' });
    }
    
    res.json(review);
});
                

This pattern follows RESTful principles, where the URL structure reflects the hierarchical relationship between resources (products have many reviews).

Query Parameters

Query parameters are used to send optional data to the server as key-value pairs in the URL after a question mark. They're accessible via the req.query object.

Query Parameters Example


// Route that handles query parameters
app.get('/search', (req, res) => {
    const query = req.query.q;
    const category = req.query.category || 'all';
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    
    console.log(`Search query: ${query}`);
    console.log(`Category: ${category}`);
    console.log(`Page: ${page}, Limit: ${limit}`);
    
    // Search logic would go here
    
    res.send({
        query,
        category,
        page,
        limit,
        results: [`Result for ${query} in ${category}`]
    });
});
                

When a client makes a request to /search?q=express&category=framework&page=2&limit=20, the req.query object will contain:


{
    q: "express",
    category: "framework",
    page: "2",
    limit: "20"
}
                

Notice that all query parameter values are strings, which is why we need to convert numeric values using parseInt().

Real-world Example: Filtering and Pagination

Query parameters are particularly useful for implementing filtering, sorting, and pagination in APIs:


app.get('/api/products', (req, res) => {
    // Extract query parameters with defaults
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const sort = req.query.sort || 'createdAt';
    const order = req.query.order || 'desc';
    const category = req.query.category;
    const minPrice = req.query.minPrice ? parseFloat(req.query.minPrice) : undefined;
    const maxPrice = req.query.maxPrice ? parseFloat(req.query.maxPrice) : undefined;
    
    // Calculate pagination values
    const skip = (page - 1) * limit;
    
    // Build database query
    let dbQuery = {};
    
    if (category) {
        dbQuery.category = category;
    }
    
    if (minPrice !== undefined || maxPrice !== undefined) {
        dbQuery.price = {};
        if (minPrice !== undefined) dbQuery.price.$gte = minPrice;
        if (maxPrice !== undefined) dbQuery.price.$lte = maxPrice;
    }
    
    // Example: Fetch products from database
    // const products = await Product.find(dbQuery)
    //     .sort({ [sort]: order === 'asc' ? 1 : -1 })
    //     .skip(skip)
    //     .limit(limit);
    
    // const totalProducts = await Product.countDocuments(dbQuery);
    
    // Mock response for demonstration
    const products = [`Product for query: ${JSON.stringify(dbQuery)}`];
    const totalProducts = 100;
    
    res.json({
        products,
        pagination: {
            currentPage: page,
            totalPages: Math.ceil(totalProducts / limit),
            totalItems: totalProducts,
            itemsPerPage: limit
        }
    });
});
                

This endpoint could be accessed with a URL like /api/products?category=electronics&minPrice=100&maxPrice=500&page=2&limit=10&sort=price&order=asc to get the second page of electronics products priced between $100 and $500, sorted by price in ascending order.

Route Handlers

Route handlers are functions that are executed when a matching route is found. They have access to the request and response objects, as well as the next middleware function.

Single Handler Function


app.get('/user/:id', (req, res) => {
    const userId = req.params.id;
    // Logic to fetch user by ID
    res.send(`User details for ID: ${userId}`);
});
            

Multiple Handler Functions

You can provide multiple handler functions that behave like middleware for a specific route:


function validateUserId(req, res, next) {
    // Validate user ID format
    const userId = req.params.id;
    if (!userId.match(/^[0-9a-fA-F]{24}$/)) {
        return res.status(400).send('Invalid user ID format');
    }
    next();
}

function checkUserExists(req, res, next) {
    // Check if user exists in database
    const userId = req.params.id;
    const user = findUserById(userId);
    
    if (!user) {
        return res.status(404).send('User not found');
    }
    
    // Attach user to request object for later use
    req.user = user;
    next();
}

function respondWithUser(req, res) {
    // User is already attached to request by previous middleware
    res.json(req.user);
}

// Route with multiple handlers
app.get('/user/:id', validateUserId, checkUserExists, respondWithUser);
            

This approach allows for better code organization and reusability. Each function performs a specific task, following the single responsibility principle.

Express Router

The Express Router is a class that helps organize routes into modular, mountable route handlers. It works like a mini application, capable of performing middleware and routing functions.

graph TD A[Express Application] --> B[User Router] A --> C[Product Router] A --> D[Order Router] B --> E[GET /users] B --> F[POST /users] B --> G[GET /users/:id] C --> H[GET /products] C --> I[POST /products] C --> J[GET /products/:id] D --> K[GET /orders] D --> L[POST /orders] D --> M[GET /orders/:id] style A fill:#f9d71c,stroke:#333,stroke-width:2px style B fill:#a1ffa8,stroke:#333,stroke-width:2px style C fill:#a1ffa8,stroke:#333,stroke-width:2px style D fill:#a1ffa8,stroke:#333,stroke-width:2px

Creating and Using Express Router


const express = require('express');
const app = express();

// Create a router instance
const userRouter = express.Router();

// Define routes on the router
userRouter.get('/', (req, res) => {
    res.send('Get all users');
});

userRouter.post('/', (req, res) => {
    res.send('Create a new user');
});

userRouter.get('/:id', (req, res) => {
    res.send(`Get user with ID: ${req.params.id}`);
});

userRouter.put('/:id', (req, res) => {
    res.send(`Update user with ID: ${req.params.id}`);
});

userRouter.delete('/:id', (req, res) => {
    res.send(`Delete user with ID: ${req.params.id}`);
});

// Mount the router on the app
app.use('/users', userRouter);

app.listen(3000, () => {
    console.log('Server running on port 3000');
});
                

In this example, all routes defined on the userRouter are prefixed with /users when mounted on the app. This allows for better organization of route handlers by resource type.

Modular Route Organization

In a real-world application, you would typically organize routes in separate files:

File: routes/userRoutes.js


const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');

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

module.exports = router;
                

File: routes/productRoutes.js


const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');

// Product routes
router.get('/', productController.getAllProducts);
router.post('/', productController.createProduct);
router.get('/:id', productController.getProductById);
router.put('/:id', productController.updateProduct);
router.delete('/:id', productController.deleteProduct);

module.exports = router;
                

File: app.js


const express = require('express');
const userRoutes = require('./routes/userRoutes');
const productRoutes = require('./routes/productRoutes');

const app = express();

// Mount route modules
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);

app.listen(3000, () => {
    console.log('Server running on port 3000');
});
                

This approach creates a clean, modular, and maintainable codebase. Each resource has its own router file, and related controller logic is separated into controller files.

Middleware Deep Dive

Let's explore middleware in more depth, as it's one of the most powerful features of Express.js.

The Production Line Analogy

Middleware in Express is like a production line in a factory:

  • Raw materials (the HTTP request) enter the factory
  • Each workstation (middleware function) performs a specific operation
  • The product moves from one workstation to the next in a predefined order
  • Each workstation can modify the product, reject defective products, or pass them along
  • At the end of the line, the finished product (HTTP response) is shipped out

Just as a factory can have different production lines for different products, Express can have different middleware chains for different routes.

Middleware Execution Flow

graph LR A[Request] --> B[Middleware 1] B -->|next()| C[Middleware 2] C -->|next()| D[Middleware 3] D -->|next()| E[Route Handler] E --> F[Response] B -->|response sent| F C -->|response sent| F D -->|response sent| F style F fill:#a1ffa8,stroke:#333,stroke-width:2px

Key points about middleware execution:

Middleware Functions in Detail

A middleware function has access to three objects:

Anatomy of a Middleware Function


function middleware(req, res, next) {
    // 1. Process the request
    console.log(`Request received: ${req.method} ${req.url}`);
    
    // 2. Modify request or response objects (optional)
    req.customProperty = 'some value';
    
    // 3. End the request-response cycle (optional)
    if (someCondition) {
        return res.status(403).send('Forbidden');
    }
    
    // 4. Call the next middleware
    next();
}
            

Common Middleware Operations

Middleware Examples


// Logging middleware
app.use((req, res, next) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
    next();
});

// Request time tracking middleware
app.use((req, res, next) => {
    req.requestTime = Date.now();
    next();
});

// Authentication middleware
app.use((req, res, next) => {
    const authHeader = req.headers.authorization;
    
    if (!authHeader) {
        return res.status(401).json({ error: 'Authorization header required' });
    }
    
    const [type, token] = authHeader.split(' ');
    
    if (type !== 'Bearer' || !token) {
        return res.status(401).json({ error: 'Invalid authorization format' });
    }
    
    // Verify token (simplified example)
    if (token === 'valid-token') {
        req.user = { id: '123', name: 'John Doe' };
        next();
    } else {
        res.status(401).json({ error: 'Invalid token' });
    }
});

// Response time header middleware
app.use((req, res, next) => {
    const start = Date.now();
    
    // Listen for when response is finished
    res.on('finish', () => {
        const duration = Date.now() - start;
        console.log(`Request to ${req.url} took ${duration}ms`);
    });
    
    next();
});
                

These examples demonstrate different types of middleware for logging, request augmentation, authentication, and performance monitoring.

Error Handling Middleware

Error handling middleware in Express has a special signature with four parameters: err, req, res, and next. Express recognizes this as error-handling middleware by the number of arguments.

Basic Error Handling


app.get('/users/:id', (req, res, next) => {
    // Example: trying to find a user
    const userId = req.params.id;
    
    // If user not found, create an error
    if (userId === '0') {
        // Pass error to the next middleware
        const err = new Error('User not found');
        err.statusCode = 404;
        return next(err);
    }
    
    res.send(`User: ${userId}`);
});

// Error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    
    // Set status code (default to 500 if not specified)
    const statusCode = err.statusCode || 500;
    
    // Send error response
    res.status(statusCode).json({
        error: {
            message: err.message,
            status: statusCode
        }
    });
});
                

When next() is called with an argument, Express skips all non-error middleware and passes control to the first error-handling middleware.

Comprehensive Error Handling

In a production application, you might implement more sophisticated error handling:


// Custom Error Classes
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
        this.isOperational = true;
        
        Error.captureStackTrace(this, this.constructor);
    }
}

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

class ValidationError extends AppError {
    constructor(message = 'Validation failed') {
        super(message, 400);
    }
}

// Route that might generate errors
app.get('/api/users/:id', async (req, res, next) => {
    try {
        const user = await findUserById(req.params.id);
        
        if (!user) {
            throw new NotFoundError(`User with ID ${req.params.id} not found`);
        }
        
        res.json(user);
    } catch (err) {
        next(err); // Pass error to error handling middleware
    }
});

// Error handling middleware
app.use((err, req, res, next) => {
    // Log error for developers
    console.error('ERROR 💥', err);
    
    // Default values
    err.statusCode = err.statusCode || 500;
    err.status = err.status || 'error';
    
    // Different responses based on environment
    if (process.env.NODE_ENV === 'development') {
        // Detailed error for development
        res.status(err.statusCode).json({
            status: err.status,
            error: err,
            message: err.message,
            stack: err.stack
        });
    } else {
        // Simplified error for production
        if (err.isOperational) {
            // Operational, trusted error: send message to client
            res.status(err.statusCode).json({
                status: err.status,
                message: err.message
            });
        } else {
            // Programming or other unknown error: don't leak error details
            console.error('ERROR 💥', err);
            res.status(500).json({
                status: 'error',
                message: 'Something went wrong'
            });
        }
    }
});
                

This approach uses custom error classes to standardize error handling across the application. It also distinguishes between operational errors (expected errors like "user not found") and programming errors (unexpected bugs).

Built-in Middleware

Express provides several built-in middleware functions that handle common tasks:

express.json()

Parses incoming requests with JSON payloads and makes the data available on req.body.


app.use(express.json());

app.post('/api/users', (req, res) => {
    console.log(req.body); // Contains parsed JSON data
    res.json({ received: req.body });
});
            

express.urlencoded()

Parses incoming requests with URL-encoded payloads (typically from HTML forms) and makes the data available on req.body.


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

app.post('/login', (req, res) => {
    const { username, password } = req.body;
    console.log(`Login attempt: ${username}`);
    res.send(`Form submission received: ${username}`);
});
            

express.static()

Serves static files such as HTML, CSS, images, and JavaScript files.


app.use(express.static('public'));

// Now files in the 'public' directory are available at the root path
// For example, 'public/css/style.css' is accessible at '/css/style.css'
            

You can also specify a virtual path prefix:


app.use('/static', express.static('public'));

// Now files in the 'public' directory are available under the '/static' path
// For example, 'public/css/style.css' is accessible at '/static/css/style.css'
            

Popular Third-party Middleware

Many third-party middleware packages extend Express's functionality:

morgan

HTTP request logger middleware with various logging formats.


const morgan = require('morgan');

// Use predefined format
app.use(morgan('dev'));  // :method :url :status :response-time ms

// Custom format
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
            

helmet

Helps secure Express apps by setting various HTTP headers.


const helmet = require('helmet');

// Use helmet with default configuration
app.use(helmet());

// Or configure specific headers
app.use(
    helmet({
        contentSecurityPolicy: {
            directives: {
                defaultSrc: ["'self'"],
                scriptSrc: ["'self'", "example.com"]
            }
        },
        xssFilter: true
    })
);
            

cors

Enables Cross-Origin Resource Sharing (CORS).


const cors = require('cors');

// Enable CORS for all routes
app.use(cors());

// Configure CORS for specific origins
app.use(cors({
    origin: ['https://example.com', 'https://subdomain.example.com'],
    methods: ['GET', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization']
}));

// CORS for a specific route
app.get('/api/public-data', cors(), (req, res) => {
    res.json({ data: 'This data is accessible from any origin' });
});
            

compression

Compresses response bodies for improved performance.


const compression = require('compression');

// Use compression middleware
app.use(compression());

// With options
app.use(compression({
    level: 6,  // Compression level (0-9)
    threshold: 1024  // Only compress responses larger than 1KB
}));
            

cookie-parser

Parses Cookie header and populates req.cookies with an object keyed by cookie names.


const cookieParser = require('cookie-parser');

// Basic usage
app.use(cookieParser());

// With a secret for signed cookies
app.use(cookieParser('your-secret-key'));

app.get('/get-cookie', (req, res) => {
    console.log('Cookies:', req.cookies);  // Regular cookies
    console.log('Signed Cookies:', req.signedCookies);  // Signed cookies
    
    res.send('Check the console for cookies');
});

app.get('/set-cookie', (req, res) => {
    // Set a regular cookie
    res.cookie('user', 'john', { maxAge: 900000, httpOnly: true });
    
    // Set a signed cookie
    res.cookie('authenticated', 'true', { signed: true });
    
    res.send('Cookies set');
});
            

Creating Custom Middleware

You can create your own middleware functions to handle specific requirements of your application.

Basic Custom Middleware


// Request logger middleware
function requestLogger(req, res, next) {
    const timestamp = new Date().toISOString();
    const method = req.method;
    const url = req.url;
    const ip = req.ip || req.connection.remoteAddress;
    
    console.log(`[${timestamp}] ${method} ${url} - ${ip}`);
    next();
}

// Use the custom middleware
app.use(requestLogger);
                

This simple middleware logs details about each incoming request.

Middleware with Configuration Options


// Rate limiting middleware factory
function rateLimit(options = {}) {
    const {
        windowMs = 60 * 1000, // 1 minute
        max = 100,            // 100 requests per window
        message = 'Too many requests, please try again later.'
    } = options;
    
    // Store request counts for each IP
    const requestCounts = {};
    
    // Clean up old entries every minute
    setInterval(() => {
        const now = Date.now();
        Object.keys(requestCounts).forEach(ip => {
            if (now - requestCounts[ip].timestamp > windowMs) {
                delete requestCounts[ip];
            }
        });
    }, 60 * 1000);
    
    // Return the middleware function
    return (req, res, next) => {
        const ip = req.ip || req.connection.remoteAddress;
        
        // Initialize or update request count
        if (!requestCounts[ip]) {
            requestCounts[ip] = {
                count: 1,
                timestamp: Date.now()
            };
        } else {
            const entry = requestCounts[ip];
            const timeSinceFirstRequest = Date.now() - entry.timestamp;
            
            if (timeSinceFirstRequest > windowMs) {
                // Reset if window has passed
                entry.count = 1;
                entry.timestamp = Date.now();
            } else {
                // Increment count
                entry.count++;
            }
        }
        
        // Check if limit exceeded
        if (requestCounts[ip].count > max) {
            return res.status(429).send(message);
        }
        
        next();
    };
}

// Use the rate limiting middleware
app.use(rateLimit({
    windowMs: 15 * 60 * 1000,  // 15 minutes
    max: 100                   // 100 requests per 15 minutes
}));

// Use with different settings for specific routes
app.use('/api/login', rateLimit({
    windowMs: 60 * 60 * 1000,  // 1 hour
    max: 5                     // 5 login attempts per hour
}));
                

This example creates a configurable rate limiting middleware that restricts the number of requests a client can make within a specified time window.

Real-world Example: Authentication Middleware

In a real application, you might implement authentication middleware to protect routes:


const jwt = require('jsonwebtoken');

function authenticate(options = {}) {
    const {
        required = true,
        roles = []
    } = options;
    
    return (req, res, next) => {
        // Get token from header
        const authHeader = req.headers.authorization;
        let token;
        
        if (authHeader && authHeader.startsWith('Bearer ')) {
            token = authHeader.split(' ')[1];
        }
        
        // If token is missing
        if (!token) {
            if (required) {
                return res.status(401).json({
                    status: 'fail',
                    message: 'Authentication required. Please provide a token.'
                });
            } else {
                // Token is optional, proceed without authentication
                return next();
            }
        }
        
        try {
            // Verify token
            const decoded = jwt.verify(token, process.env.JWT_SECRET);
            
            // Check if user exists (in a real app, you'd query the database)
            const user = findUserById(decoded.id);
            if (!user) {
                return res.status(401).json({
                    status: 'fail',
                    message: 'The user for this token no longer exists.'
                });
            }
            
            // Check if token is expired or user changed password
            if (decoded.iat < user.passwordChangedAt) {
                return res.status(401).json({
                    status: 'fail',
                    message: 'User recently changed password. Please log in again.'
                });
            }
            
            // Check role-based access
            if (roles.length > 0 && !roles.includes(user.role)) {
                return res.status(403).json({
                    status: 'fail',
                    message: 'You do not have permission to perform this action.'
                });
            }
            
            // Grant access - attach user to request
            req.user = user;
            next();
        } catch (err) {
            if (err.name === 'JsonWebTokenError') {
                return res.status(401).json({
                    status: 'fail',
                    message: 'Invalid token. Please log in again.'
                });
            }
            if (err.name === 'TokenExpiredError') {
                return res.status(401).json({
                    status: 'fail',
                    message: 'Your token has expired. Please log in again.'
                });
            }
            return res.status(500).json({
                status: 'error',
                message: 'Something went wrong'
            });
        }
    };
}

// Use the authentication middleware
app.get(
    '/api/user/profile',
    authenticate(),  // Token required
    (req, res) => {
        res.json({
            status: 'success',
            data: {
                user: req.user
            }
        });
    }
);

// Optional authentication
app.get(
    '/api/products',
    authenticate({ required: false }),  // Token optional
    (req, res) => {
        // If authenticated, could show personalized products
        const products = getProducts(req.user);
        res.json({
            status: 'success',
            data: {
                products
            }
        });
    }
);

// Role-based access control
app.delete(
    '/api/products/:id',
    authenticate({ roles: ['admin', 'manager'] }),  // Only certain roles can access
    (req, res) => {
        deleteProduct(req.params.id);
        res.json({
            status: 'success',
            message: 'Product deleted'
        });
    }
);
                

This authentication middleware verifies JWT tokens, checks user permissions, and protects routes based on configurable options.

Practical Exercises

Try these exercises to practice and reinforce your understanding of Express routing and middleware:

Exercise 1: Create a Router-based API

Objective: Create a RESTful API using Express Routers for different resources.

Tasks:

  1. Create a Node.js project and install Express
  2. Create separate router files for 'users' and 'posts' resources
  3. Implement CRUD operations for each resource
  4. Mount the routers at appropriate paths in your main app file
  5. Test your API using a tool like Postman or cURL

Exercise 2: Custom Authentication Middleware

Objective: Implement a simple authentication system using custom middleware.

Tasks:

  1. Create a middleware function that checks for an API key in request headers
  2. If a valid API key is provided, allow the request to proceed
  3. If no API key or an invalid one is provided, return a 401 Unauthorized response
  4. Apply this middleware to protect certain routes in your application
  5. Create both protected and public routes to test your middleware

Exercise 3: Error Handling System

Objective: Implement a comprehensive error handling system for an Express application.

Tasks:

  1. Create custom error classes for different types of errors (e.g., NotFoundError, ValidationError)
  2. Implement a global error handling middleware
  3. Add try/catch blocks to your route handlers and pass errors to the error handler
  4. Create routes that deliberately trigger different types of errors
  5. Format error responses differently based on error type and environment (development/production)

Further Resources