Express.js Mastery / Express: Let’s Start Building The Natours API!

Chaining Multiple Middleware Functions

πŸ“– Context

Up until now, only one middleware function has been passed per route (e.g. createTour for handling POST requests). This lecture covers how to run multiple middleware functions in sequence, for the same route β€” a technique that will be used constantly throughout the rest of the course.


πŸ’‘ Why do this?

Similar to the earlier checkID param middleware (which kept ID validation out of the route handlers), it's useful to validate the incoming request body before it ever reaches the createTour handler β€” keeping that handler focused only on the job of actually creating a tour.


🎯 The Challenge

Build a checkBody middleware that:

  1. βœ… Checks whether req.body contains both a name property and a price property.

  2. ❌ If either is missing, sends back a 400 (Bad Request) response.

  3. ➑️ If both are present, calls next() to proceed to createTour.


πŸ”Ή Step 1: Chaining Middleware Syntax

router
  .route('/')
  .get(tourController.getAllTours)
  .post(checkBody, tourController.createTour);

πŸ’‘ Why this works:

  • πŸ”— Multiple functions can simply be passed as additional arguments, separated by commas.

  • ➑️ Express runs them in order: checkBody executes first, and only if it calls next() does createTour run afterward.

  • βš™οΈ This is the same chaining mechanism already used implicitly with global middleware β€” just applied directly at the route level, for a single route.


πŸ”Ή Step 2: Implementing checkBody in tourController.js file:

exports.checkBody = (req, res, next) => {
  if (!req.body.name || !req.body.price) {
    return res.status(400).json({
      status: 'fail',
      message: 'Missing name or price'
    });
  }
  next();
};

πŸ’‘ Why each part works:

  • βœ… !req.body.name || !req.body.price checks if either required field is missing.

  • 🚫 400 Bad Request indicates the client sent invalid data.

  • ⚠️ status: 'fail' is used for client-side errors.

  • πŸ›‘ return prevents the code from continuing after sending the response.

  • ➑️ next() runs only when both name and price are provided.


πŸ”Ή Step 3: Wiring It into the Router in tourRoutes.js file:

router
  .route('/')
  .get(tourController.getAllTours)
  .post(tourController.checkBody, tourController.createTour);

πŸ”Ή Step 4: Testing

πŸ§ͺ Test 1 β€” Missing Price

πŸ“€ Sending a POST request with name, duration, and difficulty, but no price β†’

βœ… Response:

  • 🚫 400 Bad Request

  • πŸ’¬ "Missing name or price"

This confirms checkBody ran, caught the missing field, and stopped the request from ever reaching createTour.


πŸ§ͺ Test 2 β€” With Price Included

πŸ“€ Sending the same request but now including a price value β†’

βœ… checkBody's condition is no longer true, so it calls next(), which passes control to createTour

πŸŽ‰ The tour is created successfully, returning:

  • βœ… 201 Created


πŸ’‘ Why This Pattern Matters

This same chaining approach can be extended for many other use cases before a resource is created or modified β€” for example:

  • πŸ” Checking if a user is logged in.

  • πŸ›‘οΈ Checking if a user has the correct permissions/access rights to perform the action.

  • βœ… Any other validation that shouldn't clutter the main handler.

🎯 Core Principle

Logic unrelated to a handler's core responsibility should live in separate middleware functions, chained before the handler β€” keeping each handler function narrowly focused on doing exactly one thing (e.g. createTour should only be concerned with creating a tour, not validating input).


βœ… Key Takeaway

  • πŸ”— A route can have multiple middleware functions, executed in the order they're listed.

  • ➑️ Each middleware must call next() to continue the request.

  • ⚠️ Use return when sending a response to prevent further execution.

  • βš™οΈ Express uses this middleware chaining pattern throughout the app.

  • πŸš€ The same approach will be used later for features like authentication and authorization.