Express.js Middleware

Middleware is one of the most powerful ideas in Express.js. Every request that reaches your server passes through a chain of middleware functions before it gets a response. Each middleware function can inspect the request, modify it, run code, and either pass control to the next function or stop the chain by sending a response.

The Airport Security Checkpoint Analogy

Imagine an airport. Every passenger (request) must pass through multiple checkpoints before boarding (getting a response). The ID check desk verifies identity. The luggage scanner checks for prohibited items. The boarding pass scanner confirms the ticket. If any checkpoint fails, the passenger stops there and goes no further. If all checks pass, the passenger boards the plane.

Passenger enters airport (request arrives)
          │
          ▼
  ┌─────────────────┐
  │  ID Check       │  ← Middleware 1: verify token
  │  (auth check)   │
  └────────┬────────┘
           │ next()
           ▼
  ┌─────────────────┐
  │  Luggage Scan   │  ← Middleware 2: validate body
  │  (body parser)  │
  └────────┬────────┘
           │ next()
           ▼
  ┌─────────────────┐
  │  Boarding Gate  │  ← Route Handler: send response
  │  (route logic)  │
  └─────────────────┘
           │
           ▼
    Passenger boards (response sent)

Middleware Function Signature

Every middleware function takes three parameters:

function myMiddleware(req, res, next) {
  // Do something with req or res
  next(); // Pass control to the next middleware
}
  • req — the request object
  • res — the response object
  • next — a function that passes control to the next middleware in the chain

Calling next() is critical. Without it, the request hangs and the browser waits forever for a response.

Registering Middleware with app.use()

Use app.use() to register middleware that runs for every request:

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

// This middleware runs for EVERY request
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
  next(); // Pass control forward
});

app.get('/', (req, res) => {
  res.send('Home Page');
});

app.listen(3000);

Every time anyone visits any URL, the terminal logs the method, path, and timestamp. Then next() passes control to the route handler.

Types of Middleware

1. Application-Level Middleware

Bound to the app object with app.use() or app.get(). Runs for all requests or for a specific path:

// Runs for all requests
app.use((req, res, next) => {
  console.log('Request received');
  next();
});

// Runs only for /admin routes
app.use('/admin', (req, res, next) => {
  console.log('Admin area accessed');
  next();
});

2. Built-In Middleware

Express ships with several useful middleware functions ready to use:

┌────────────────────────────────────────────────────────────┐
│              Express Built-In Middleware                   │
├────────────────────────────┬───────────────────────────────┤
│ express.json()             │ Parses JSON request bodies    │
│ express.urlencoded()       │ Parses HTML form data         │
│ express.static()           │ Serves files from a folder    │
│ express.Router()           │ Creates modular route groups  │
└────────────────────────────┴───────────────────────────────┘
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));

3. Third-Party Middleware

The npm ecosystem offers many middleware packages that solve common problems:

┌────────────────────────────────────────────────────────────┐
│             Popular Third-Party Middleware                 │
├───────────────┬────────────────────────────────────────────┤
│ morgan        │ Logs every HTTP request automatically      │
│ cors          │ Enables cross-origin requests              │
│ helmet        │ Adds security headers                      │
│ cookie-parser │ Parses cookies from the request            │
│ multer        │ Handles file uploads                       │
│ express-rate  │ Limits request frequency                   │
│  -limit       │                                            │
└───────────────┴────────────────────────────────────────────┘
const morgan = require('morgan');
app.use(morgan('dev')); // Logs: GET / 200 5.123 ms - 12

4. Error-Handling Middleware

Error middleware has four parameters: (err, req, res, next). Express recognizes it as an error handler because of the four-parameter signature:

app.use((err, req, res, next) => {
  console.error(err.message);
  res.status(500).json({ error: 'Something went wrong' });
});

Place error middleware at the very end of your middleware chain, after all routes.

The Middleware Execution Order

Express runs middleware in the exact order you define it. Order matters:

app.use(loggerMiddleware);     // Runs first
app.use(authMiddleware);       // Runs second
app.use(bodyParserMiddleware); // Runs third
app.get('/home', handler);     // Runs last (only if path matches)

If you place express.json() after your route handlers, req.body will be empty in those handlers because parsing happens too late.

Stopping the Chain: Sending a Response from Middleware

Middleware can stop the chain by sending a response directly instead of calling next(). This is useful for authentication:

const checkAuth = (req, res, next) => {
  const token = req.headers['authorization'];

  if (!token) {
    // Stop here — send 401, do NOT call next()
    return res.status(401).json({ error: 'No token provided' });
  }

  // Token exists — continue to the next middleware or route
  next();
};

app.get('/dashboard', checkAuth, (req, res) => {
  res.send('Welcome to your dashboard');
});

If the token is missing, the request never reaches the route handler. The middleware itself sends the 401 response and the chain ends.

Applying Middleware to Specific Routes

Pass middleware as an argument directly to a route instead of using app.use() to apply it globally:

// Only /dashboard needs auth
app.get('/dashboard', checkAuth, (req, res) => {
  res.send('Private dashboard');
});

// /home is public — no checkAuth here
app.get('/home', (req, res) => {
  res.send('Public home page');
});

Multiple Middleware Functions in One Route

Stack multiple middleware functions in a single route by passing them as additional arguments:

const logRequest = (req, res, next) => {
  console.log('Logging request...');
  next();
};

const validateBody = (req, res, next) => {
  if (!req.body.name) {
    return res.status(400).json({ error: 'Name required' });
  }
  next();
};

const createUser = (req, res) => {
  res.status(201).json({ message: `User ${req.body.name} created` });
};

app.post('/users', logRequest, validateBody, createUser);

The three functions run in order: log, validate, create. Each one calls next() to pass control to the following function.

Modifying the Request Object in Middleware

Middleware can attach data to the req object so later handlers can access it:

const addTimestamp = (req, res, next) => {
  req.requestTime = new Date().toISOString();
  next();
};

app.use(addTimestamp);

app.get('/info', (req, res) => {
  res.json({ requestedAt: req.requestTime });
});

Summary

Middleware functions form a chain that every request passes through before reaching a route handler. Each middleware receives req, res, and next. Calling next() passes control forward. Sending a response stops the chain. Express includes built-in middleware like express.json() and express.static(). Third-party packages like morgan and helmet add powerful features with one line of code. Error middleware uses four parameters and lives at the end of the chain. Apply middleware globally with app.use() or selectively by passing it as a route argument.

Leave a Comment

Your email address will not be published. Required fields are marked *