Middleware Patterns in Node.js

· 3 min read · Updated March 18, 2026 · intermediate
node express middleware javascript backend beginner

Middleware is the backbone of Express.js. It’s the mechanism that lets you inject logic into the request-response cycle, making your application flexible and modular. This tutorial covers the fundamental middleware patterns you need to build robust Node.js applications.

What Is Middleware?

Middleware functions are functions that have access to three things: the request object (req), the response object (res), and the next function in the application’s request-response cycle.

A middleware function can:

  • Execute any code
  • Make changes to the request or response objects
  • End the request-response cycle
  • Call the next middleware in the stack

If a middleware doesn’t end the cycle, it must call next() to pass control to the next middleware. Forgetting to call next() is a common mistake that leaves requests hanging.

The Basic Middleware Signature

Every middleware function follows this signature:

function middlewareName(req, res, next) {
  // Your logic here
  next(); // Pass control to the next middleware
}

The three parameters are conventionally named req, res, and next. You can name them anything, but sticking to the convention makes your code readable to other developers.

Creating Your First Middleware

Here’s a simple logging middleware that logs when a request hits your server:

const myLogger = function (req, res, next) {
  console.log('Request received:', req.method, req.url);
  next();
};

app.use(myLogger);

The order matters. If you load this middleware after a route, the request never reaches it because the route handler terminates the cycle first.

Middleware That Modifies the Request

You can add properties to the request object that downstream handlers can use:

const requestTime = function (req, res, next) {
  req.requestTime = Date.now();
  next();
};

app.use(requestTime);

app.get('/', (req, res) => {
  res.send(`Page rendered at: ${new Date(req.requestTime)}`);
});

This pattern is useful for adding timestamps, user information from authentication, or any data computed early in the cycle.

Configurable Middleware

Create reusable middleware by exporting a function that accepts options:

// my-middleware.js
module.exports = function (options) {
  return function (req, res, next) {
    // Use options.foo, options.bar here
    if (options.required && !req.headers[options.required]) {
      return res.status(400).send('Missing required header');
    }
    next();
  };
};

// Usage
const mw = require('./my-middleware');
app.use(mw({ required: 'x-api-key' }));

This pattern is how popular middleware like cors and helmet work — they accept configuration and return a middleware function.

Error Handling Middleware

Error handling middleware is special. It takes four parameters instead of three:

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something went wrong!');
});

Express identifies error handlers by their four-parameter signature. When you call next(err), Express skips all regular middleware and routes, going straight to your error handler.

Async Middleware

Async middleware requires special handling. If your middleware returns a Promise, Express 5 will automatically call next() with the rejection reason:

// Express 5 (automatic)
async function validateCookies(req, res, next) {
  await checkCookies(req.cookies);
  next();
}

// Express 4 (manual error handling)
function validateCookies(req, res, next) {
  checkCookies(req.cookies)
    .then(() => next())
    .catch(next); // Pass error to Express
}

In Express 4, always catch async errors and pass them to next(). Otherwise, unhandled rejections crash your server.

Chaining Middleware

The power of middleware comes from chaining. Each middleware can:

  • Perform its task and call next()
  • Send a response and end the cycle
  • Pass an error to next(err)
app.use(checkAuth);      // 1. Verify user is authenticated
app.use(loadUser);        // 2. Load user data into req.user
app.use(logRequest);      // 3. Log the request
app.get('/profile', (req, res) => {  // 4. Handle the route
  res.json(req.user);
});

This chain pattern is how authentication, logging, parsing, and routing all work together cleanly.

Third-Party Middleware

Express includes minimal built-in middleware. For common tasks, use proven third-party packages:

PackagePurpose
morganHTTP request logging
helmetSecurity headers
corsCross-origin resource sharing
cookie-parserParse cookies
body-parserParse request bodies
const helmet = require('helmet');
const morgan = require('morgan');

app.use(helmet());
app.use(morgan('tiny'));

See Also