What Is Middleware in Express.js? How It Works With Code Examples

What Is Middleware in Express.js? How It Works With Code Examples

by | Oct 6, 2026 | Uncategorized | 0 comments

If you have ever looked at an Express app and wondered what all those app.use() lines actually do, you are looking at the middleware stack. Express middleware is the mechanism that turns a raw HTTP request into a finished response, one small function at a time.

This guide explains the concept from zero, with short annotated snippets you can paste straight into your own project, including a custom request logger and a centralized error handler.

What is middleware in Express.js? (short answer)

Middleware in Express is a function that receives the request object (req), the response object (res) and a next function, and runs at some point during the request-response cycle. It can read the request, modify it, attach data to it, send a response, or hand control to the next function in the stack by calling next().

Here is the smallest possible middleware:

// A middleware is just a function with 3 parameters
function sayHello(req, res, next) {
  console.log('A request arrived at', req.originalUrl);
  next(); // pass control to the next middleware in the stack
}

That is it. No class, no interface, no magic. The only rule Express enforces is the shape of the arguments.

javascript code laptop

The request-response cycle, in plain English

When a request hits your server, Express does not run your route handler directly. It walks through a stack of middleware functions in the order they were registered. Each function has three options:

  1. Do some work and call next(), which passes control to the next function in the stack.
  2. End the cycle by sending a response (res.send(), res.json(), res.redirect(), and so on).
  3. Call next(err) with an argument, which skips every remaining normal middleware and jumps to the error-handling middleware.

If a middleware does none of the three, the request hangs until the client times out. That is the single most common beginner bug.

A typical stack looks like this:

Incoming request
   ↓
express.json()          // parse JSON body
   ↓
logger()                // your custom logger
   ↓
authenticate()          // checks a token, attaches req.user
   ↓
router (GET /users/:id) // your route handler sends the response
   ↓
errorHandler(err, ...)  // only reached if something calls next(err)

Why order matters

Express executes middleware in the order it is added. Registering a body parser after your routes means req.body will be undefined inside those routes.

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

app.use(express.json());          // 1. runs first for every request
app.use('/api', apiRouter);       // 2. only for URLs starting with /api
app.use(notFoundHandler);         // 3. nothing matched above
app.use(errorHandler);            // 4. error handler goes LAST

app.listen(3000);

What does next() really do?

next() is not a return statement. It tells Express “I am done, move on”, but the code after it still executes.

app.use((req, res, next) => {
  next();
  console.log('This line STILL runs'); // often a source of confusion
});

The safe habit is to return when you branch:

app.use((req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'Missing token' }); // ends the cycle
  }
  return next(); // continue to the next middleware
});

Variations of next worth knowing:

Call Effect
next() Go to the next matching middleware or route.
next(err) Skip to the first error-handling middleware (4 arguments).
next('route') Skip the remaining handlers of the current route and try the next matching route.
next('router') Leave the current router entirely and continue in the parent stack.
javascript code laptop

The types of Express middleware

The official documentation lists five categories. Here is the overview before we look at the three that matter most day to day.

Type Bound to Typical use
Application-level app.use() / app.get() Logging, CORS, body parsing, global auth
Router-level router.use() Rules that apply to one group of routes
Error-handling app.use((err, req, res, next)) One central place to format errors
Built-in Shipped with Express express.json(), express.urlencoded(), express.static()
Third-party npm packages helmet, cors, morgan, cookie-parser

1. Application-level middleware

Registered on the app instance. With no path, it runs for every request; with a path, only for URLs that start with it.

// Runs for EVERY request
app.use((req, res, next) => {
  req.requestTime = Date.now();
  next();
});

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

// Middleware attached to a single route, before the handler
app.get('/profile', requireAuth, (req, res) => {
  res.json({ user: req.user, at: req.requestTime });
});

You can also pass an array or several functions in a row. They execute left to right.

2. Router-level middleware

Exactly the same idea, but attached to an express.Router() instance. This keeps feature-specific rules out of your main file.

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

// applies to every route defined in THIS router only
router.use((req, res, next) => {
  console.log('users router:', req.method, req.originalUrl);
  next();
});

router.get('/', (req, res) => res.json([{ id: 1, name: 'Ada' }]));
router.get('/:id', (req, res) => res.json({ id: req.params.id }));

module.exports = router;
// app.js
const usersRouter = require('./routes/users');
app.use('/api/users', usersRouter); // mounted with a prefix

Rule of thumb: if a check applies to one resource, put it in the router. If it applies to the whole API, put it on the app.

3. Error-handling middleware

Error middleware is identified by its four parameters. If you write three, Express treats it as normal middleware and your errors will never reach it.

app.use((err, req, res, next) => {   // 4 arguments = error handler
  console.error(err);
  res.status(500).json({ error: 'Something broke' });
});

Even if you do not use next, you must declare it. Always register error handlers after all routes.

Drop-in example 1: a custom request logger

A tiny logger that measures how long each request took and prints method, URL, status and duration. No dependencies.

// middleware/logger.js
function logger(req, res, next) {
  const start = process.hrtime.bigint();

  // 'finish' fires once the response has been sent
  res.on('finish', () => {
    const ms = Number(process.hrtime.bigint() - start) / 1e6;
    const line = [
      new Date().toISOString(),
      req.method,
      req.originalUrl,
      res.statusCode,
      ms.toFixed(1) + 'ms'
    ].join(' ');
    console.log(line);
  });

  next(); // never forget this, or the request will hang
}

module.exports = logger;
// app.js
const logger = require('./middleware/logger');
app.use(logger); // register it early so it sees every request

Sample output:

2026-09-05T06:12:44.108Z GET /api/users 200 4.7ms
2026-09-05T06:12:46.902Z POST /api/users 422 11.3ms

Drop-in example 2: a centralized error handler

Three pieces work together: a small error class, a 404 catcher and the handler itself.

// middleware/AppError.js
class AppError extends Error {
  constructor(message, statusCode = 500, details = null) {
    super(message);
    this.statusCode = statusCode;
    this.details = details;
    this.isOperational = true; // errors we expect, not bugs
  }
}
module.exports = AppError;
// middleware/errorHandler.js
const AppError = require('./AppError');

// Runs when no route matched the URL
function notFound(req, res, next) {
  next(new AppError('Route ' + req.originalUrl + ' not found', 404));
}

// The single place where every error is formatted
function errorHandler(err, req, res, next) {
  // If headers are already sent, delegate to the default Express handler
  if (res.headersSent) return next(err);

  const status = err.statusCode || 500;
  const isProd = process.env.NODE_ENV === 'production';

  if (status >= 500) console.error(err.stack);

  res.status(status).json({
    error: {
      message: status >= 500 && isProd ? 'Internal server error' : err.message,
      details: err.details || undefined,
      stack: isProd ? undefined : err.stack
    }
  });
}

module.exports = { notFound, errorHandler };
// app.js (order is critical)
const { notFound, errorHandler } = require('./middleware/errorHandler');

app.use('/api/users', usersRouter);

app.use(notFound);      // after all routes
app.use(errorHandler);  // very last

Now any route can simply throw:

router.get('/:id', async (req, res, next) => {
  const user = await db.findUser(req.params.id);
  if (!user) throw new AppError('User not found', 404);
  res.json(user);
});

Async errors: Express 5 versus Express 4

In Express 5, a rejected promise returned by an async handler is forwarded to your error middleware automatically. In Express 4, it is not, and the request hangs. If you are still on 4, wrap your handlers:

// middleware/asyncHandler.js  (only needed on Express 4)
const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

module.exports = asyncHandler;

// usage
router.get('/:id', asyncHandler(async (req, res) => { /* ... */ }));
javascript code laptop

Writing reusable middleware with options

Most npm middleware packages are functions that return a middleware. That pattern lets you configure behaviour once. expressjs.com published something useful on the subject.

// middleware/requireRole.js
function requireRole(...allowedRoles) {
  return function (req, res, next) {
    if (!req.user) return next(new AppError('Not authenticated', 401));
    if (!allowedRoles.includes(req.user.role)) {
      return next(new AppError('Forbidden', 403));
    }
    next();
  };
}
module.exports = requireRole;

// usage
router.delete('/:id', requireRole('admin', 'owner'), deleteUser);

Common Express middleware mistakes

  • Forgetting next(): the request hangs forever with no error message.
  • Calling next() after sending a response: produces “Cannot set headers after they are sent”.
  • Declaring the error handler with 3 parameters: it silently becomes normal middleware.
  • Registering the error handler before the routes: it will never be reached.
  • Putting express.json() after the routes: req.body stays undefined.
  • Heavy work in a global middleware: every request pays the cost, even static assets.
  • Mutating req with generic names: prefer req.user or a namespaced object over req.data.
javascript code laptop

A recommended middleware order

  1. Security headers (helmet)
  2. CORS
  3. Request logger
  4. Body parsers (express.json(), express.urlencoded())
  5. Cookies and sessions
  6. Static files (express.static())
  7. Rate limiting
  8. Authentication and authorization
  9. Routers and route handlers
  10. 404 handler
  11. Centralized error handler

Key takeaways

  • Middleware is just a function of (req, res, next) that sits in an ordered stack.
  • Each function must either respond, call next(), or call next(err).
  • Application-level middleware is global, router-level is scoped to a group of routes, error-handling middleware has four parameters and goes last.
  • A logger and a single error handler are the two pieces of custom middleware almost every project needs on day one.

Once the mental model clicks, the rest of the Express ecosystem becomes easy to read: authentication, validation, caching and rate limiting are all the same pattern applied to different problems.

FAQ about Express middleware

What is middleware in Express with a simple example?

It is a function that runs between the incoming request and the final response. Example: app.use((req, res, next) => { console.log(req.method); next(); }); logs every request method, then hands control to the next function.

What is the difference between middleware and a route handler?

Technically none. A route handler is simply the last middleware in the chain, the one that usually sends the response instead of calling next().

How many middleware functions can an Express app have?

There is no hard limit. What matters is that each one is fast, because they run sequentially for every matching request.

Why does my request hang and never respond?

Almost always because a middleware neither sent a response nor called next(). Add a log line at the top and bottom of each middleware to find where the chain stops.

Do I need body-parser as a separate package?

No. Since Express 4.16, express.json() and express.urlencoded() are built in. Related reading: How to Use Middleware Effectively in Express.

Which third-party middleware should every developer know?

helmet for security headers, cors for cross-origin requests, morgan for logging, cookie-parser for cookies, and a rate limiter such as express-rate-limit.

Is Express still worth learning?

Yes. It remains the most widely used Node.js web framework, and its middleware model is the reference that many newer frameworks imitate, so the concepts transfer well. Originally covered on https://flaviocopes.com.

Does the middleware order really change behaviour?

Yes, completely. Express walks the stack top to bottom in registration order, so a parser, an auth check or an error handler placed in the wrong position simply will not do its job.