Express.js Routing and Middleware

Understanding the core building blocks of Express applications

Introduction to Routing

Routing is one of the most fundamental aspects of Express.js. It determines how an application responds to client requests to specific endpoints, which are defined by URIs (or paths) and HTTP methods (GET, POST, PUT, DELETE, etc.).

What is Routing?

Routing refers to how an application's endpoints (URIs) respond to client requests. Each route can have one or more handler functions, which are executed when the route is matched.

flowchart TD A[Client Request] --> B{Routing Layer} B -->|GET /users| C[User List Handler] B -->|POST /users| D[Create User Handler] B -->|GET /products| E[Product List Handler] B -->|GET /not-found| F[404 Handler] C --> G[Response] D --> G E --> G F --> G

Basic Route Syntax

Express routes follow a simple pattern:

app.METHOD(PATH, HANDLER);

Where:

Basic Routing Examples

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

// Respond with "Hello World!" for requests to the root URL (/)
app.get('/', (req, res) => {
  res.send('Hello World!');
});

// Respond to POST request on the root route
app.post('/', (req, res) => {
  res.send('Got a POST request');
});

// Respond to PUT request to /user route
app.put('/user', (req, res) => {
  res.send('Got a PUT request at /user');
});

// Respond to DELETE request to /user route
app.delete('/user', (req, res) => {
  res.send('Got a DELETE request at /user');
});

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

HTTP Methods and Their Common Uses

Method Description Typical Use
GET Request data from a resource Retrieve data (read-only)
POST Submit data to be processed Create new resources
PUT Update a resource Replace an existing resource
PATCH Partially update a resource Modify parts of an existing resource
DELETE Delete a resource Remove resources
OPTIONS Get allowed communication options CORS preflight requests
HEAD Same as GET but without response body Check if resource exists without transferring data

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 with one parameter
app.get('/users/:userId', (req, res) => {
  res.send(`User ID: ${req.params.userId}`);
});

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

// Example URLs and their params:
// GET /users/34           -> req.params.userId = "34"
// GET /users/45/books/123 -> req.params.userId = "45", req.params.bookId = "123"

Parameter Constraints with Regular Expressions

You can constrain route parameters to match specific patterns using regular expressions:

// Only match if userId is a 5-digit number
app.get('/users/:userId(\\d{5})', (req, res) => {
  res.send(`User ID: ${req.params.userId}`);
});

// Only match if username contains lowercase letters and numbers only
app.get('/users/:username([a-z0-9]+)', (req, res) => {
  res.send(`Username: ${req.params.username}`);
});

Real-World Example: Product Catalog API

// Route for product categories
app.get('/products/categories/:categoryName', (req, res) => {
  const { categoryName } = req.params;
  
  // Get products by category (mock implementation)
  const products = getProductsByCategory(categoryName);
  
  res.json(products);
});

// Route for specific product by ID (with validation)
app.get('/products/:productId(\\d+)', (req, res) => {
  const productId = parseInt(req.params.productId);
  
  // Get product details (mock implementation)
  const product = getProductById(productId);
  
  if (!product) {
    return res.status(404).json({ message: 'Product not found' });
  }
  
  res.json(product);
});

// Route for product reviews
app.get('/products/:productId/reviews', (req, res) => {
  const { productId } = req.params;
  
  // Get reviews for product (mock implementation)
  const reviews = getProductReviews(productId);
  
  res.json(reviews);
});

Query Parameters

Query parameters are key-value pairs in the URL after the question mark (?). They're typically used for filtering, sorting, pagination, or optional parameters.

// Route handling query parameters
app.get('/products', (req, res) => {
  // Access query parameters
  const { category, minPrice, maxPrice, sort } = req.query;
  
  console.log('Query Parameters:');
  console.log('Category:', category);
  console.log('Min Price:', minPrice);
  console.log('Max Price:', maxPrice);
  console.log('Sort:', sort);
  
  // Filter products based on query params (mock implementation)
  let products = getAllProducts();
  
  if (category) {
    products = products.filter(p => p.category === category);
  }
  
  if (minPrice) {
    products = products.filter(p => p.price >= parseFloat(minPrice));
  }
  
  if (maxPrice) {
    products = products.filter(p => p.price <= parseFloat(maxPrice));
  }
  
  if (sort === 'price-asc') {
    products.sort((a, b) => a.price - b.price);
  } else if (sort === 'price-desc') {
    products.sort((a, b) => b.price - a.price);
  }
  
  res.json(products);
});

// Example URLs:
// GET /products?category=electronics
// GET /products?minPrice=10&maxPrice=50
// GET /products?category=clothing&sort=price-asc

Route Parameters vs Query Parameters

Route Parameters Query Parameters
Part of the URL path Appended after the ? in the URL
Typically required Optional
Used for resource identification Used for filtering, sorting, pagination
Example: /users/:id Example: /users?role=admin&active=true
Accessed via req.params Accessed via req.query

Best Practices for Working with Query Parameters

  • Set defaults for query parameters when they're not provided
  • Validate and sanitize query parameter values (they are user input!)
  • Convert types as needed (query parameters are always strings)
  • Use clear, descriptive names for query parameters
  • Document expected query parameters in your API documentation
// Example with proper validation and defaults
app.get('/api/search', (req, res) => {
  // Set defaults and convert types
  const query = req.query.q || '';
  const page = parseInt(req.query.page || '1', 10);
  const limit = parseInt(req.query.limit || '10', 10);
  const sortBy = ['name', 'date', 'relevance'].includes(req.query.sort) 
    ? req.query.sort 
    : 'relevance';
  
  // Validate values
  if (page < 1) {
    return res.status(400).json({ message: 'Page must be greater than 0' });
  }
  
  if (limit < 1 || limit > 100) {
    return res.status(400).json({ message: 'Limit must be between 1 and 100' });
  }
  
  if (query.length < 2) {
    return res.status(400).json({ message: 'Search query must be at least 2 characters' });
  }
  
  // Calculate offset for pagination
  const offset = (page - 1) * limit;
  
  // Process search (mock implementation)
  const results = searchItems(query, { offset, limit, sortBy });
  const total = countSearchResults(query);
  
  // Return results with pagination metadata
  res.json({
    results,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit)
    }
  });
});

Route Handlers

Route handlers are the functions that execute when a matching route is found. You can provide multiple handler functions that behave like middleware for a route.

Single Handler Function

app.get('/users', (req, res) => {
  // Handle request for users list
  res.json([
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ]);
});

Multiple Handler Functions

// Authentication middleware
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    return res.status(401).json({ message: 'Authorization required' });
  }
  
  // Simplified auth check (for demonstration)
  if (authHeader === 'Bearer valid-token') {
    // Store user info in request object
    req.user = { id: 123, role: 'admin' };
    next(); // Pass control to the next handler
  } else {
    res.status(401).json({ message: 'Invalid token' });
  }
}

// Permission check middleware
function checkAdmin(req, res, next) {
  if (req.user && req.user.role === 'admin') {
    next(); // User is admin, proceed to the next handler
  } else {
    res.status(403).json({ message: 'Admin access required' });
  }
}

// Route with multiple handlers
app.get('/admin/settings', authenticate, checkAdmin, (req, res) => {
  // This will only execute if both middleware functions pass
  res.json({ 
    message: 'Admin settings',
    user: req.user
  });
});
flowchart LR A[Request] --> B[authenticate] B -- "next()" --> C[checkAdmin] B -- "res.status(401)" --> Z[Response: Unauthorized] C -- "next()" --> D[Route Handler] C -- "res.status(403)" --> Y[Response: Forbidden] D --> X[Response: Success]

Handler Arrays

You can also provide an array of handler functions:

const validateUser = [
  // Check if required fields exist
  (req, res, next) => {
    const { username, email, password } = req.body;
    if (!username || !email || !password) {
      return res.status(400).json({ message: 'Missing required fields' });
    }
    next();
  },
  
  // Validate username format
  (req, res, next) => {
    const { username } = req.body;
    if (!/^[a-zA-Z0-9_]{3,16}$/.test(username)) {
      return res.status(400).json({ 
        message: 'Username must be 3-16 characters and contain only letters, numbers, and underscores' 
      });
    }
    next();
  },
  
  // Validate email format
  (req, res, next) => {
    const { email } = req.body;
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      return res.status(400).json({ message: 'Invalid email format' });
    }
    next();
  },
  
  // Validate password strength
  (req, res, next) => {
    const { password } = req.body;
    if (password.length < 8) {
      return res.status(400).json({ message: 'Password must be at least 8 characters' });
    }
    next();
  }
];

// Use the validation array in a route
app.post('/users', validateUser, (req, res) => {
  // If we get here, all validations passed
  const { username, email, password } = req.body;
  
  // Create user (mock implementation)
  const user = createUser({ username, email, password });
  
  res.status(201).json({
    message: 'User created successfully',
    userId: user.id
  });
});

Router Object

The Express Router object is a complete middleware and routing system that allows you to create modular, mountable route handlers. This is especially useful for organizing routes in larger applications.

Creating a Router Instance

const express = require('express');
const router = express.Router();

// Define routes on the router
router.get('/', (req, res) => {
  res.send('Home page');
});

router.get('/about', (req, res) => {
  res.send('About page');
});

// Export the router
module.exports = router;

Using the Router in Your Application

const express = require('express');
const app = express();
const mainRoutes = require('./routes/main');

// Mount the router on a specific path
app.use('/', mainRoutes);

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

Organizing Routes by Resource

A common pattern is to organize routes by resource type:

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

// All routes here are prefixed with /users due to how it's mounted
router.get('/', (req, res) => {
  res.send('List all users');
});

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

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

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

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

module.exports = router;
// routes/products.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.send('List all products');
});

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

// More product routes...

module.exports = router;
// app.js
const express = require('express');
const app = express();
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');

// Mount routers on specific paths
app.use('/users', userRoutes);
app.use('/products', productRoutes);

app.listen(3000, () => {
  console.log('Server started on port 3000');
});
flowchart TD A[app.js] --> B[Mount /users] A --> C[Mount /products] B --> D[users.js Router] C --> E[products.js Router] D --> F[GET /users] D --> G[GET /users/:id] D --> H[POST /users] D --> I[PUT /users/:id] D --> J[DELETE /users/:id] E --> K[GET /products] E --> L[GET /products/:id] E --> M[...]

Advanced Router Usage: API Versioning

One common application of the Router object is API versioning:

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

router.get('/', (req, res) => {
  res.json({ version: 'v1', users: ['John', 'Jane'] });
});

module.exports = router;
// routes/api/v2/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  // V2 provides more detailed user objects
  res.json({ 
    version: 'v2', 
    users: [
      { id: 1, name: 'John', email: 'john@example.com' },
      { id: 2, name: 'Jane', email: 'jane@example.com' }
    ] 
  });
});

module.exports = router;
// app.js
const express = require('express');
const app = express();

// Import API route modules
const usersV1 = require('./routes/api/v1/users');
const usersV2 = require('./routes/api/v2/users');

// Mount API routes with versioning
app.use('/api/v1/users', usersV1);
app.use('/api/v2/users', usersV2);

// Redirect root API requests to latest version
app.get('/api', (req, res) => {
  res.redirect('/api/v2');
});

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

Router-level Middleware

Just like with the main Express app, you can define middleware that applies only to a specific router:

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

// Middleware specific to admin routes
router.use((req, res, next) => {
  // Check if user is admin
  if (!req.user || req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Admin access required' });
  }
  next();
});

// All routes below will only be accessible to admins
router.get('/dashboard', (req, res) => {
  res.send('Admin Dashboard');
});

router.get('/reports', (req, res) => {
  res.send('Admin Reports');
});

module.exports = router;

Introduction to Middleware

Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application's request-response cycle.

What is Middleware?

Middleware functions can perform the following tasks:

  • Execute any code
  • Make changes to the request and response objects
  • End the request-response cycle
  • Call the next middleware function in the stack
flowchart LR A[Request] --> B[Middleware 1] B -- "next()" --> C[Middleware 2] C -- "next()" --> D[Middleware 3] D -- "next()" --> E[Route Handler] E --> F[Response]

The Assembly Line Analogy

Think of Express middleware as stations on an assembly line:

  • The raw materials (incoming request) enter one end of the line
  • Each station (middleware function) performs a specific operation
  • Each station can modify the product as it passes through
  • Each station can decide to pass the product to the next station or reject it
  • At the end of the line, the finished product (response) is sent back

Just as a car might skip certain assembly stations based on its model, requests can follow different middleware paths based on their characteristics.

Middleware Function Signature

function myMiddleware(req, res, next) {
  // Middleware logic here
  console.log('Middleware executed!');
  
  // If you want to pass control to the next middleware
  next();
  
  // Or if you want to end the request-response cycle
  // res.send('Response from middleware');
}

Application-level Middleware

Application-level middleware is bound to an instance of the express application using app.use() or app.METHOD():

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

// Middleware without a mount path (applies to all routes)
app.use((req, res, next) => {
  console.log('Time:', Date.now());
  next();
});

// Middleware with a mount path (applies only to specific paths)
app.use('/user/:id', (req, res, next) => {
  console.log('Request Type:', req.method);
  next();
});

// Middleware in a route handler
app.get('/user/:id', (req, res, next) => {
  console.log('User ID:', req.params.id);
  next();
}, (req, res) => {
  res.send('User Info');
});

Router-level Middleware

Router-level middleware works in the same way as application-level middleware, except it is bound to an instance of express.Router():

const express = require('express');
const router = express.Router();

// Router-level middleware
router.use((req, res, next) => {
  console.log('Router Time:', Date.now());
  next();
});

router.get('/user/:id', (req, res) => {
  res.send('User');
});

// Use the router in the app
app.use('/', router);

Built-in Middleware

Express comes with several built-in middleware functions that can be used to handle common tasks:

express.json()

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

// Parse JSON bodies
app.use(express.json());

// Now you can access JSON data in req.body
app.post('/api/users', (req, res) => {
  console.log(req.body); // Contains parsed JSON data
  res.json({ message: 'User created', data: req.body });
});

express.urlencoded()

Parses incoming requests with URL-encoded payloads (e.g., form submissions) and makes the parsed data available on req.body.

// Parse URL-encoded bodies (form data)
app.use(express.urlencoded({ extended: false }));

// Now you can access form data in req.body
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  // Process login
  res.redirect('/dashboard');
});

The extended Option

The extended: true option allows for rich objects and arrays to be encoded into the URL-encoded format, allowing for a JSON-like experience with URL-encoded. When extended: false, the URL-encoded data is parsed with the Node.js querystring library (simpler, but more limited).

express.static()

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

// Serve static files from the 'public' directory
app.use(express.static('public'));

// Serve static files with a virtual path prefix
app.use('/static', express.static('public'));

// Serve static files with an absolute path
const path = require('path');
app.use('/static', express.static(path.join(__dirname, 'public')));

Example: Serving Multiple Static Directories

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

// Serve static files from multiple directories
app.use(express.static(path.join(__dirname, 'public')));
app.use('/vendor', express.static(path.join(__dirname, 'node_modules')));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

// Example of how files would be accessed:
// public/css/style.css -> http://localhost:3000/css/style.css
// node_modules/bootstrap/dist/css/bootstrap.min.css -> http://localhost:3000/vendor/bootstrap/dist/css/bootstrap.min.css
// uploads/profile.jpg -> http://localhost:3000/uploads/profile.jpg

Custom Middleware

Custom middleware allows you to add functionality specific to your application needs.

Logging Middleware

// Simple logging middleware
function requestLogger(req, res, next) {
  const timestamp = new Date().toISOString();
  const method = req.method;
  const url = req.originalUrl || req.url;
  const ip = req.ip || req.connection.remoteAddress;
  
  console.log(`[${timestamp}] ${method} ${url} from ${ip}`);
  
  // Pass control to the next middleware
  next();
}

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

Error Handling Middleware

Error handling middleware has a special signature with four arguments (err, req, res, next):

// Error handling middleware
function errorHandler(err, req, res, next) {
  // Log the error
  console.error(err.stack);
  
  // Set appropriate status code
  const statusCode = err.statusCode || 500;
  
  // Send error response
  res.status(statusCode).json({
    status: 'error',
    message: err.message || 'An unexpected error occurred',
    // Only include error stack in development
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
}

// Use the error handler (should be defined last)
app.use(errorHandler);

// Example route that triggers an error
app.get('/error', (req, res, next) => {
  // Create a custom error
  const error = new Error('Sample error');
  error.statusCode = 400;
  
  // Pass the error to the error handler
  next(error);
});

Authentication Middleware

// JWT authentication middleware
function authenticate(req, res, next) {
  // Get the token from the Authorization header
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ message: 'Authentication required' });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    // Verify the token (using a library like jsonwebtoken)
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Add the user data to the request object
    req.user = decoded;
    
    // Proceed to the next middleware or route handler
    next();
  } catch (error) {
    res.status(401).json({ message: 'Invalid token' });
  }
}

// Use the middleware for specific routes
app.get('/api/profile', authenticate, (req, res) => {
  res.json({
    user: req.user,
    profile: { /* user profile data */ }
  });
});

Rate Limiting Middleware

// Simple in-memory rate limiter
function rateLimiter(options = {}) {
  const { windowMs = 60000, maxRequests = 100, message = 'Too many requests' } = options;
  const requests = new Map();
  
  return (req, res, next) => {
    const ip = req.ip || req.connection.remoteAddress;
    const now = Date.now();
    
    // Clean up old requests
    if (requests.has(ip)) {
      const userRequests = requests.get(ip).filter(time => now - time < windowMs);
      requests.set(ip, userRequests);
      
      if (userRequests.length >= maxRequests) {
        return res.status(429).json({ 
          message,
          retryAfter: Math.ceil((windowMs - (now - userRequests[0])) / 1000)
        });
      }
    } else {
      requests.set(ip, []);
    }
    
    // Add current request timestamp
    requests.get(ip).push(now);
    
    next();
  };
}

// Use the rate limiter
app.use('/api/', rateLimiter({
  windowMs: 60 * 1000, // 1 minute
  maxRequests: 100      // 100 requests per minute
}));

// For more sensitive routes, use a stricter limiter
app.use('/api/auth/', rateLimiter({
  windowMs: 15 * 60 * 1000, // 15 minutes
  maxRequests: 5            // 5 requests per 15 minutes
}));

Note on Production Rate Limiting

For production applications, consider using established rate limiting middleware like express-rate-limit or rate-limiter-flexible instead of implementing your own. These packages provide more features, better performance, and are well-tested in production environments.

Third-Party Middleware

Express has a rich ecosystem of third-party middleware that you can use to add functionality to your application.

Middleware Purpose Installation
morgan HTTP request logger npm install morgan
cors Cross-Origin Resource Sharing npm install cors
helmet Security headers npm install helmet
compression Response compression npm install compression
express-session Session management npm install express-session
cookie-parser Parse cookie header npm install cookie-parser
multer File uploads npm install multer

Examples of Using Third-Party Middleware

Morgan (HTTP Request Logger)

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

// Use morgan middleware with predefined format
app.use(morgan('dev'));

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

CORS (Cross-Origin Resource Sharing)

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

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

// Or configure CORS with options
app.use(cors({
  origin: 'https://example.com',
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true
}));

// Or enable CORS for specific routes
app.get('/api/public', cors(), (req, res) => {
  res.json({ message: 'This is public' });
});

Helmet (Security Headers)

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

// Use all default helmet middleware
app.use(helmet());

// Or configure specific middleware
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", 'trusted-cdn.com']
      }
    },
    xssFilter: true,
    noSniff: true
  })
);

Express Session (Session Management)

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

app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: { 
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Using the session in routes
app.get('/login', (req, res) => {
  // Set session data
  req.session.user = { id: 1, username: 'john' };
  res.redirect('/dashboard');
});

app.get('/dashboard', (req, res) => {
  // Access session data
  if (!req.session.user) {
    return res.redirect('/login');
  }
  
  res.send(`Welcome, ${req.session.user.username}!`);
});

app.get('/logout', (req, res) => {
  // Destroy session
  req.session.destroy();
  res.redirect('/');
});

Multer (File Uploads)

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

// Configure storage
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },
  filename: (req, file, cb) => {
    cb(null, `${Date.now()}-${file.originalname}`);
  }
});

// Configure file filter
const fileFilter = (req, file, cb) => {
  // Accept only images
  if (file.mimetype.startsWith('image/')) {
    cb(null, true);
  } else {
    cb(new Error('Not an image! Please upload an image.'), false);
  }
};

// Create upload middleware
const upload = multer({ 
  storage,
  fileFilter,
  limits: {
    fileSize: 1024 * 1024 * 5 // 5MB limit
  }
});

// Single file upload
app.post('/upload/profile', upload.single('avatar'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ message: 'No file uploaded' });
  }
  
  res.json({
    message: 'File uploaded successfully',
    file: req.file
  });
});

// Multiple files upload
app.post('/upload/gallery', upload.array('photos', 5), (req, res) => {
  if (!req.files || req.files.length === 0) {
    return res.status(400).json({ message: 'No files uploaded' });
  }
  
  res.json({
    message: `${req.files.length} files uploaded successfully`,
    files: req.files
  });
});

// Error handling for multer
app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    return res.status(400).json({
      message: err.message
    });
  }
  next(err);
});

Middleware Execution Order

The order in which middleware is defined is significant because middleware functions are executed sequentially.

sequenceDiagram participant Client participant Express participant Logger participant BodyParser participant Auth participant Route Client->>Express: HTTP Request Express->>Logger: Pass request Logger->>Logger: Log request details Logger->>BodyParser: next() BodyParser->>BodyParser: Parse request body BodyParser->>Auth: next() Auth->>Auth: Verify authentication Auth->>Route: next() Route->>Route: Process request Route->>Client: HTTP Response
const express = require('express');
const app = express();

// This middleware will execute first
app.use((req, res, next) => {
  console.log('Middleware 1');
  next();
});

// This middleware will execute second
app.use((req, res, next) => {
  console.log('Middleware 2');
  next();
});

// This middleware will execute third
app.use((req, res, next) => {
  console.log('Middleware 3');
  next();
});

// This route handler will execute last
app.get('/', (req, res) => {
  console.log('Route handler');
  res.send('Hello World');
});

// Output for a GET request to /:
// Middleware 1
// Middleware 2
// Middleware 3
// Route handler

Conditional Next

You can conditionally call next() to control the flow of execution:

app.use((req, res, next) => {
  if (req.headers.authorization) {
    // Process the request normally
    next();
  } else {
    // Skip the next middleware and send a response
    res.status(401).send('Authorization required');
  }
});

Breaking the Chain

If you don't call next() or end the response (res.send(), res.json(), etc.), the request will hang:

// BAD EXAMPLE - request will hang
app.use((req, res, next) => {
  if (someCondition) {
    next();
  }
  // Missing else clause! Request will hang if condition is false
});

// GOOD EXAMPLE
app.use((req, res, next) => {
  if (someCondition) {
    next();
  } else {
    // Always end the response if not calling next()
    res.status(400).send('Bad request');
  }
});

Best Practices for Middleware Order

  1. Add logging middleware early to capture all requests
  2. Add security middleware (helmet, cors) early to protect all routes
  3. Add body parsing middleware (express.json, express.urlencoded) before routes that need parsed data
  4. Add session/cookie middleware before routes that need them
  5. Add authentication middleware before protected routes
  6. Add route-specific middleware directly to the routes that need them
  7. Add error handling middleware last to catch any errors
// Example of effective middleware ordering
const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const session = require('express-session');

const app = express();

// Logging middleware (first to capture all requests)
app.use(morgan('dev'));

// Security middleware
app.use(helmet());
app.use(cors());

// Request parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Session middleware
app.use(session({ /* config */ }));

// Serving static files
app.use(express.static('public'));

// Application routes
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);

// 404 handler for unmatched routes
app.use((req, res, next) => {
  res.status(404).json({ message: 'Route not found' });
});

// Error handling middleware (last)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: 'Internal server error' });
});

Practical Exercise

Exercise: Build a Blog API with Express Router and Custom Middleware

In this exercise, you'll build a simple blog API using Express routers and custom middleware.

Project Structure

blog-api/
  ├── app.js
  ├── middleware/
  │   ├── logger.js
  │   ├── auth.js
  │   └── errorHandler.js
  ├── routes/
  │   ├── posts.js
  │   └── comments.js
  └── data/
      ├── posts.js
      └── comments.js

Step 1: Initialize the Project

mkdir blog-api
cd blog-api
npm init -y
npm install express uuid

Step 2: Create the Data Files

Create a data directory and add the following files:

data/posts.js:

// Initial sample data
let posts = [
  {
    id: '1',
    title: 'Introduction to Express',
    content: 'Express is a minimal and flexible Node.js web application framework...',
    author: 'John Doe',
    timestamp: '2025-05-01T10:00:00.000Z'
  },
  {
    id: '2',
    title: 'Working with Middleware',
    content: 'Middleware functions are functions that have access to the request object...',
    author: 'Jane Smith',
    timestamp: '2025-05-02T14:30:00.000Z'
  }
];

module.exports = {
  getAll: () => posts,
  getById: (id) => posts.find(post => post.id === id),
  create: (post) => {
    posts.push(post);
    return post;
  },
  update: (id, updatedPost) => {
    const index = posts.findIndex(post => post.id === id);
    if (index === -1) return null;
    
    posts[index] = { ...posts[index], ...updatedPost };
    return posts[index];
  },
  delete: (id) => {
    const index = posts.findIndex(post => post.id === id);
    if (index === -1) return false;
    
    posts.splice(index, 1);
    return true;
  }
};

data/comments.js:

// Initial sample data
let comments = [
  {
    id: '1',
    postId: '1',
    content: 'Great introduction to Express!',
    author: 'Alice',
    timestamp: '2025-05-01T11:30:00.000Z'
  },
  {
    id: '2',
    postId: '1',
    content: 'Very helpful article.',
    author: 'Bob',
    timestamp: '2025-05-01T12:15:00.000Z'
  },
  {
    id: '3',
    postId: '2',
    content: 'Middleware is so powerful!',
    author: 'Charlie',
    timestamp: '2025-05-02T15:45:00.000Z'
  }
];

module.exports = {
  getAll: () => comments,
  getByPostId: (postId) => comments.filter(comment => comment.postId === postId),
  create: (comment) => {
    comments.push(comment);
    return comment;
  },
  update: (id, updatedComment) => {
    const index = comments.findIndex(comment => comment.id === id);
    if (index === -1) return null;
    
    comments[index] = { ...comments[index], ...updatedComment };
    return comments[index];
  },
  delete: (id) => {
    const index = comments.findIndex(comment => comment.id === id);
    if (index === -1) return false;
    
    comments.splice(index, 1);
    return true;
  }
};

Step 3: Create the Middleware Functions

Create a middleware directory and add the following files:

middleware/logger.js:

// Logger middleware
function logger(req, res, next) {
  const timestamp = new Date().toISOString();
  const method = req.method;
  const url = req.originalUrl || req.url;
  
  console.log(`[${timestamp}] ${method} ${url}`);
  
  // Add request timestamp to the request object
  req.requestTime = timestamp;
  
  next();
}

module.exports = logger;

middleware/auth.js:

// Simulated authentication middleware
// In a real application, this would verify tokens, session cookies, etc.
function authenticate(req, res, next) {
  // For demonstration, we'll use a simple API key check
  const apiKey = req.headers['x-api-key'];
  
  if (!apiKey || apiKey !== 'secret-api-key') {
    return res.status(401).json({ message: 'Authentication required' });
  }
  
  // Set user info (in a real app, this would come from a token or database)
  req.user = {
    id: '123',
    name: 'Admin User',
    role: 'admin'
  };
  
  next();
}

// Optional middleware for specific roles
function requireRole(role) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ message: 'Authentication required' });
    }
    
    if (req.user.role !== role) {
      return res.status(403).json({ message: `Role '${role}' required for this action` });
    }
    
    next();
  };
}

module.exports = {
  authenticate,
  requireRole
};

middleware/errorHandler.js:

// Error handling middleware
function errorHandler(err, req, res, next) {
  // Log the error
  console.error(`Error: ${err.message}`);
  console.error(err.stack);
  
  // Determine status code
  const statusCode = err.statusCode || 500;
  
  // Send error response
  res.status(statusCode).json({
    status: 'error',
    message: err.message || 'Internal Server Error'
  });
}

// 404 handler middleware
function notFoundHandler(req, res, next) {
  const error = new Error(`Not Found - ${req.originalUrl}`);
  error.statusCode = 404;
  next(error);
}

module.exports = {
  errorHandler,
  notFoundHandler
};

Step 4: Create the Route Files

Create a routes directory and add the following files:

routes/posts.js:

const express = require('express');
const router = express.Router();
const { v4: uuidv4 } = require('uuid');
const posts = require('../data/posts');
const { authenticate, requireRole } = require('../middleware/auth');

// GET all posts
router.get('/', (req, res) => {
  res.json(posts.getAll());
});

// GET a specific post
router.get('/:id', (req, res) => {
  const post = posts.getById(req.params.id);
  
  if (!post) {
    return res.status(404).json({ message: 'Post not found' });
  }
  
  res.json(post);
});

// POST a new post (requires authentication)
router.post('/', authenticate, (req, res) => {
  const { title, content } = req.body;
  
  if (!title || !content) {
    return res.status(400).json({ message: 'Title and content are required' });
  }
  
  const newPost = {
    id: uuidv4(),
    title,
    content,
    author: req.user.name,
    timestamp: new Date().toISOString()
  };
  
  posts.create(newPost);
  res.status(201).json(newPost);
});

// PUT (update) a post (requires authentication)
router.put('/:id', authenticate, (req, res) => {
  const { title, content } = req.body;
  const post = posts.getById(req.params.id);
  
  if (!post) {
    return res.status(404).json({ message: 'Post not found' });
  }
  
  // Only allow updates to own posts or if admin
  if (post.author !== req.user.name && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'You can only update your own posts' });
  }
  
  const updatedPost = posts.update(req.params.id, { title, content });
  
  res.json(updatedPost);
});

// DELETE a post (requires admin role)
router.delete('/:id', authenticate, requireRole('admin'), (req, res) => {
  const success = posts.delete(req.params.id);
  
  if (!success) {
    return res.status(404).json({ message: 'Post not found' });
  }
  
  res.status(204).end();
});

module.exports = router;

routes/comments.js:

const express = require('express');
const router = express.Router();
const { v4: uuidv4 } = require('uuid');
const comments = require('../data/comments');
const posts = require('../data/posts');
const { authenticate } = require('../middleware/auth');

// GET all comments for a post
router.get('/post/:postId', (req, res) => {
  const postId = req.params.postId;
  
  // Check if post exists
  const post = posts.getById(postId);
  if (!post) {
    return res.status(404).json({ message: 'Post not found' });
  }
  
  const postComments = comments.getByPostId(postId);
  res.json(postComments);
});

// POST a new comment (requires authentication)
router.post('/', authenticate, (req, res) => {
  const { postId, content } = req.body;
  
  if (!postId || !content) {
    return res.status(400).json({ message: 'Post ID and content are required' });
  }
  
  // Check if post exists
  const post = posts.getById(postId);
  if (!post) {
    return res.status(404).json({ message: 'Post not found' });
  }
  
  const newComment = {
    id: uuidv4(),
    postId,
    content,
    author: req.user.name,
    timestamp: new Date().toISOString()
  };
  
  comments.create(newComment);
  res.status(201).json(newComment);
});

// PUT (update) a comment (requires authentication)
router.put('/:id', authenticate, (req, res) => {
  const { content } = req.body;
  const commentId = req.params.id;
  
  // Find the comment in our data store
  const allComments = comments.getAll();
  const comment = allComments.find(c => c.id === commentId);
  
  if (!comment) {
    return res.status(404).json({ message: 'Comment not found' });
  }
  
  // Only allow users to update their own comments
  if (comment.author !== req.user.name && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'You can only update your own comments' });
  }
  
  const updatedComment = comments.update(commentId, { content });
  
  res.json(updatedComment);
});

// DELETE a comment (requires authentication)
router.delete('/:id', authenticate, (req, res) => {
  const commentId = req.params.id;
  
  // Find the comment in our data store
  const allComments = comments.getAll();
  const comment = allComments.find(c => c.id === commentId);
  
  if (!comment) {
    return res.status(404).json({ message: 'Comment not found' });
  }
  
  // Only allow users to delete their own comments or admins to delete any
  if (comment.author !== req.user.name && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'You can only delete your own comments' });
  }
  
  const success = comments.delete(commentId);
  
  if (!success) {
    return res.status(404).json({ message: 'Comment not found' });
  }
  
  res.status(204).end();
});

module.exports = router;

Step 5: Create the Main Application File

Create the app.js file in the root directory:

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

// Import middleware
const logger = require('./middleware/logger');
const { errorHandler, notFoundHandler } = require('./middleware/errorHandler');

// Import routes
const postsRouter = require('./routes/posts');
const commentsRouter = require('./routes/comments');

// Apply global middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(logger);

// Apply routes
app.use('/api/posts', postsRouter);
app.use('/api/comments', commentsRouter);

// Root route
app.get('/', (req, res) => {
  res.json({
    message: 'Blog API',
    endpoints: {
      posts: '/api/posts',
      comments: '/api/comments'
    },
    timestamp: req.requestTime // Added by logger middleware
  });
});

// Handle 404 errors
app.use(notFoundHandler);

// Handle all other errors
app.use(errorHandler);

// Start the server
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

module.exports = app;

Step 6: Test Your API

  1. Start your server:
    node app.js
  2. Use a tool like Postman, Insomnia, or curl to test your API endpoints:
    • GET all posts: GET http://localhost:3000/api/posts
    • GET post by ID: GET http://localhost:3000/api/posts/1
    • CREATE post: POST http://localhost:3000/api/posts with JSON body:
      {
        "title": "Testing Express Middleware",
        "content": "This is a test post created via the API."
      }

      Don't forget to include the x-api-key: secret-api-key header!

    • GET comments for a post: GET http://localhost:3000/api/comments/post/1

Bonus Challenges

  1. Add Validation Middleware: Create a middleware to validate post and comment data
  2. Add Rate Limiting: Implement rate limiting for API endpoints
  3. Add Pagination: Update the posts and comments routes to support pagination
  4. Add Search Functionality: Add the ability to search posts by title or content
  5. Add Swagger Documentation: Document your API using Swagger/OpenAPI

Summary

Further Reading

Next Lesson Preview

In our next session, we'll explore Request and Response objects in Express.js in greater detail. We'll learn about the various properties and methods available in these objects, how to handle different types of request data, and how to format and send different types of responses. We'll also cover more advanced techniques for handling HTTP headers, cookies, sessions, and file uploads.