What is Middleware? The Express Request Pipeline

Middleware is the single most important concept in Express. It is the mechanism behind JSON parsing, authentication, logging, CORS, rate limiting, error handling โ€” everything that happens to a request between the moment it arrives and the moment a response is sent. Understanding middleware deeply means you can read any Express codebase, debug request problems quickly, and compose your own reusable request processing logic. In this lesson you will learn exactly what a middleware function is, how the pipeline works, and why the order in which you register middleware is critical.

What Is Middleware?

A middleware function is any function with access to the request object (req), the response object (res), and the next function. When called, next() passes control to the next middleware in the pipeline. When not called, the pipeline stops โ€” the request is either responded to or left hanging.

// The middleware function signature
function myMiddleware(req, res, next) {
  // 1. Do something with req (read headers, parse body, verify token...)
  // 2. Optionally modify req or res (attach user, set headers...)
  // 3. Either:
  //    a. Call next() to pass to the next middleware / route handler
  //    b. Call res.json() / res.send() to end the request-response cycle
  //    c. Call next(err) to jump to the error handling middleware
  next();
}

The Request Pipeline โ€” Visualised

Incoming HTTP Request
        โ”‚
        โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  helmet()         โ”‚  โ†’ sets security headers on res
โ”‚  next()           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ”‚
          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  cors()           โ”‚  โ†’ adds Access-Control-Allow-Origin header
โ”‚  next()           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ”‚
          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  morgan()         โ”‚  โ†’ logs: GET /api/posts 200 12ms
โ”‚  next()           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ”‚
          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  express.json()   โ”‚  โ†’ parses JSON body โ†’ populates req.body
โ”‚  next()           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ”‚
          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  protect()        โ”‚  โ†’ verifies JWT โ†’ attaches req.user
โ”‚  next() or 401    โ”‚  โ†’ if no token: res.status(401).json(...)
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ”‚
          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Route Handler    โ”‚  โ†’ queries MongoDB, sends JSON response
โ”‚  res.json(data)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚
        โ–ผ
   HTTP Response sent to client
Note: Express processes middleware and routes strictly in the order they are registered with app.use() or app.get/post/.... If express.json() is registered after your routes, those routes will have an empty req.body. If cors() is registered after a route that handles OPTIONS preflight, the browser’s CORS check will fail. Order is everything.
Tip: Think of middleware as a conveyor belt. Each station (middleware function) can inspect and modify the item (request), pass it along (next()), send it off early (res.json()), or report a problem (next(err)). Stations registered earlier always run before stations registered later โ€” you design the belt by choosing what you register and in what order.
Warning: If a middleware function neither calls next() nor sends a response, the request will hang indefinitely until the client times out. This is one of the hardest bugs to spot โ€” the server receives the request, logs it, but the client never gets a response. Always make sure every code path in your middleware either calls next() or sends a response.

Three Types of Middleware in Express

Type Scope Registration Example
Application-level All routes on the app app.use(fn) cors, helmet, morgan, express.json()
Router-level All routes on a Router router.use(fn) Auth guard for an entire resource group
Route-level One specific route app.get('/path', fn, handler) Validate ObjectId only on parameterised routes

Application-Level Middleware

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

// Runs for EVERY request to the app, regardless of path or method
app.use((req, res, next) => {
  console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
  next();
});

// Runs for EVERY request to paths starting with /api
app.use('/api', (req, res, next) => {
  res.set('X-API-Version', '1.0');
  next();
});

// Routes are also middleware โ€” they just end the pipeline when matched
app.get('/api/posts', (req, res) => {
  res.json({ data: [] });
});

Router-Level Middleware

// routes/posts.js โ€” auth middleware applies to ALL post routes
const express = require('express');
const router  = express.Router();
const protect = require('../middleware/auth');

// Apply protect middleware to ALL routes in this router
router.use(protect);

// Every route below this line requires authentication
router.get('/',    getAllPosts);
router.post('/',   createPost);
router.get('/:id', getPostById);
router.put('/:id', updatePost);

Route-Level Middleware

// Middleware applies only to the routes where it is explicitly added
const protect          = require('../middleware/auth');
const validateObjectId = require('../middleware/validateObjectId');
const checkOwnership   = require('../middleware/checkOwnership');

// Public โ€” no middleware
router.get('/',    getAllPosts);
router.get('/:id', validateObjectId('id'), getPostById); // validate ID only

// Protected โ€” auth required
router.post('/',   protect, createPost);

// Protected + ownership check
router.put('/:id',    protect, validateObjectId('id'), checkOwnership, updatePost);
router.delete('/:id', protect, validateObjectId('id'), checkOwnership, deletePost);

How next() Works

// next() โ€” pass to next middleware/route
const logger = (req, res, next) => {
  console.log('Request received');
  next(); // โ† continue pipeline
};

// next(err) โ€” skip to error middleware
const protect = (req, res, next) => {
  if (!req.headers.authorization) {
    return next(new Error('No token')); // โ† jump to error handler
  }
  next(); // โ† all good, continue
};

// next('router') โ€” skip remaining middleware in current Router
// next('route')  โ€” skip remaining handlers for current route
// (advanced โ€” rarely needed in standard MERN APIs)

Common Mistakes

Mistake 1 โ€” Registering middleware after routes that need it

โŒ Wrong โ€” express.json() after the POST route means req.body is undefined:

app.post('/api/posts', (req, res) => {
  console.log(req.body); // undefined!
});
app.use(express.json()); // too late โ€” registered after the route

โœ… Correct โ€” middleware must be registered before the routes that depend on it:

app.use(express.json()); // always before routes
app.post('/api/posts', handler); // req.body is now populated โœ“

Mistake 2 โ€” Calling next() after sending a response

โŒ Wrong โ€” calling both res.json() and next() causes a “headers already sent” error:

app.use((req, res, next) => {
  res.json({ message: 'intercepted' }); // sends response
  next(); // then tries to continue pipeline โ€” ERROR
});

โœ… Correct โ€” once a response is sent, stop pipeline execution with return:

app.use((req, res, next) => {
  if (someCondition) return res.json({ message: 'intercepted' }); // stops here
  next(); // only reaches here if condition was false
});

Mistake 3 โ€” Forgetting next() in a middleware that should pass through

โŒ Wrong โ€” middleware that should log and pass on, but doesn’t:

app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  // forgot next() โ€” all requests hang here
});

โœ… Correct:

app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next(); // โœ“
});

Quick Reference

Task Code
Apply to all routes app.use(middlewareFn)
Apply to path prefix app.use('/api', middlewareFn)
Apply to router routes router.use(middlewareFn)
Apply to single route app.get('/path', mw, handler)
Pass to next middleware next()
Pass to error handler next(new Error('msg'))
End pipeline with response return res.status(401).json({...})

🧠 Test Yourself

You add app.use(express.json()) to your Express app but POST requests still have an undefined req.body. Where should you look first?