Express JS middleware chaining


Middleware Chaining in Express.js

Middleware chaining in Express.js refers to the process of executing multiple middleware functions in sequence during the lifecycle of a single HTTP request. This is achieved by calling the next() function inside each middleware, which passes control to the next middleware in the chain.

Middleware chaining allows you to break down application logic into smaller, modular units that can be executed in order. Each middleware function can either modify the request (req) and response (res) objects or terminate the request-response cycle by sending a response.

How Middleware Chaining Works

In Express.js, middleware functions are executed in the order they are declared. When a middleware function completes its task, it calls next() to pass control to the next middleware. If next() is not called, the request-response cycle will hang, meaning the request will not proceed to the next middleware or route handler.

Basic Example of Middleware Chaining

const express = require('express'); const app = express(); // First middleware const middleware1 = (req, res, next) => { console.log('Middleware 1 executed'); req.customProperty = 'Data from Middleware 1'; next(); // Passes control to the next middleware }; // Second middleware const middleware2 = (req, res, next) => { console.log('Middleware 2 executed'); console.log('Custom Property:', req.customProperty); // Access data from previous middleware next(); // Passes control to the route handler }; // Route handler const finalHandler = (req, res) => { res.send('Final handler executed'); }; // Use middleware in sequence (chaining) app.use(middleware1); app.use(middleware2); app.get('/', finalHandler); app.listen(3000, () => { console.log('Server running on port 3000'); });

Explanation of Example:

  • middleware1: Logs a message and adds a custom property to the req object.
  • middleware2: Logs a message, accesses the custom property from req, and passes control to the next step.
  • finalHandler: The final route handler that responds to the client.

Middleware functions are executed in the declared order:

  1. middleware1 runs first, modifying the req object.
  2. middleware2 runs next, accessing the modified req object.
  3. Finally, the route handler runs and sends the response.

Middleware Chaining for Route-Specific Middleware

You can apply middleware chaining directly on a route by listing multiple middleware functions. For example

app.get('/example', middleware1, middleware2, (req, res) => { res.send('Route handler after middleware chain'); });

Here, when a GET request is made to /example, the two middleware functions (middleware1 and middleware2) are executed in order before reaching the route handler.

Example: Middleware Chaining for Authentication and Logging

const isAuthenticated = (req, res, next) => { if (req.headers.authorization) { console.log('User authenticated'); next(); // Proceed to the next middleware or route } else { res.status(401).send('Unauthorized'); } }; const logRequest = (req, res, next) => { console.log(`Request to ${req.url}`); next(); // Proceed to the next middleware or route }; // Chain the middleware for the protected route app.get('/dashboard', isAuthenticated, logRequest, (req, res) => { res.send('Welcome to the dashboard'); });

In this example:

  1. isAuthenticated: Checks if the user is authenticated. If they are, it passes control to the next middleware. Otherwise, it terminates the request by sending a 401 Unauthorized response.
  2. logRequest: Logs the request URL, then passes control to the final route handler.
  3. If both middlewares complete successfully, the route handler sends a response.

Error Handling in Middleware Chains

If any middleware in the chain encounters an error, you can pass the error to the next middleware using next(err).

const express = require('express'); const app = express(); // Middleware with an error const middlewareWithError = (req, res, next) => { const error = new Error('Something went wrong!'); next(error); // Passes the error to the error-handling middleware }; // Error-handling middleware const errorHandler = (err, req, res, next) => { console.error(err.message); res.status(500).send('Internal Server Error'); }; // Route with middleware chain app.get('/', middlewareWithError, (req, res) => { res.send('This will not be reached if there is an error'); }); // Error-handling middleware should be defined after all other middleware app.use(errorHandler); app.listen(3000, () => { console.log('Server running on port 3000'); });

Execution Order of Middleware

  1. Express executes middleware in the order it is declared.
  2. Middleware must call next() to pass control to the next middleware in the stack.
  3. If a middleware doesn't call next() or send a response, the request will hang.
  4. Error-handling middleware must come after all other middleware, and it uses four parameters: err, req, res, next.

Practical Use Cases for Middleware Chaining

  • Authentication: A middleware to check if the user is logged in, followed by logging requests for auditing.
  • Request validation: A middleware to validate incoming data before passing it to the route handler.
  • Response modifications: Middleware can modify response headers or content before the final response is sent.
  • Error handling: Centralized error-handling middleware that catches errors in previous middleware or route handlers.