The best examples of middleware in Express.js: 3 practical examples for real apps

If you’re building an API or web app with Node, you can’t avoid middleware. And honestly, you shouldn’t want to. The best examples of middleware in Express.js turn messy request-handling code into small, focused, reusable functions that you can plug in anywhere in your app. In this guide, we’ll walk through examples of middleware in Express.js: 3 practical examples you’ll actually use in production, plus several more patterns worth stealing. We’ll start with logging, authentication, and error handling, then expand into real examples like request timing, rate limiting, and security headers. Along the way, you’ll see how to write your own custom middleware, how to compose multiple functions for a single route, and how modern Express code (as of 2024–2025) handles async/await without blowing up your error handling. By the end, you’ll have a mental toolbox of Express middleware patterns you can drop into your next Node project.
Written by
Jamie
Published

Let’s skip the theory and go straight to real code. These three are the best examples of middleware in Express.js that almost every production app uses:

  • Request logging
  • Authentication / authorization
  • Centralized error handling

We’ll start with these 3 practical examples, then branch into more specialized patterns.


Practical example of logging middleware (with request IDs)

A classic example of middleware in Express.js is a logger that runs on every request. In 2024 and beyond, most teams want two things from logging:

  • Structured logs (JSON, not random strings)
  • A way to trace a single request across services (request IDs)

Here’s a realistic logging middleware using morgan plus a custom request ID using crypto:

const express = require('express');
const morgan = require('morgan');
const crypto = require('crypto');

const app = express();

// 1) Request ID middleware
function requestIdMiddleware(req, res, next) {
  const existingId = req.headers['x-request-id'];
  const id = existingId || crypto.randomUUID();

  req.id = id;
  res.setHeader('X-Request-Id', id);
  next();
}

// 2) Structured logging middleware using morgan
morgan.token('id', (req) => req.id);

const loggerMiddleware = morgan(
  ':method :url :status :res[content-length] - :response-time ms :id'
);

app.use(requestIdMiddleware);
app.use(loggerMiddleware);

app.get('/health', (req, res) => {
  res.json({ status: 'ok', requestId: req.id });
});

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

This is one of the best examples of middleware in Express.js because it shows:

  • How a custom middleware (requestIdMiddleware) can enrich the req and res objects
  • How third-party middleware (morgan) fits into the same pipeline
  • How multiple middleware functions can stack for every request

In real apps, teams often ship these logs to services like Datadog, New Relic, or open-source stacks like the Elastic Stack. The Node.js documentation itself encourages structured logging for production systems.


Authentication: real examples of middleware in Express.js with JWT

Another classic example of middleware in Express.js is authentication. Instead of sprinkling jwt.verify everywhere, you wrap it in a single reusable function.

Here’s a common pattern using JSON Web Tokens (JWT) and async/await:

const jwt = require('jsonwebtoken');

function authMiddleware(requiredRole) {
  return async function (req, res, next) {
    try {
      const authHeader = req.headers.authorization || '';
      const token = authHeader.startsWith('Bearer ')
        ? authHeader.slice(7)
        : null;

      if (!token) {
        return res.status(401).json({ error: 'Missing token' });
      }

      const payload = jwt.verify(token, process.env.JWT_SECRET);
      req.user = payload;

      if (requiredRole && payload.role !== requiredRole) {
        return res.status(403).json({ error: 'Forbidden' });
      }

      next();
    } catch (err) {
      // Token invalid, expired, or verification failed
      return res.status(401).json({ error: 'Invalid token' });
    }
  };
}

// Usage examples
app.get('/profile', authMiddleware(), (req, res) => {
  res.json({ user: req.user });
});

app.get('/admin', authMiddleware('admin'), (req, res) => {
  res.json({ adminData: true });
});

These routes show real examples of middleware in Express.js:

  • /profile uses authMiddleware() with no role restriction
  • /admin uses authMiddleware('admin') to enforce a specific role

The middleware is reusable, testable, and easy to reason about. You can also plug this into route groups using express.Router() so all admin routes share the same authentication layer.

For background on why token-based auth is still widely used in 2024–2025, the NIST Digital Identity Guidelines discuss modern identity practices and risk-based access control.


Centralized error handling: the cleanup crew

If you’re using async/await, you need a consistent way to catch and format errors. Express has a specific pattern for this: an error-handling middleware with four parameters: (err, req, res, next).

Here’s a practical example of middleware in Express.js that centralizes error handling:

// Async wrapper so you don't repeat try/catch
function asyncHandler(fn) {
  return function (req, res, next) {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Example route using asyncHandler
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await UserModel.findById(req.params.id);
  if (!user) {
    const error = new Error('User not found');
    error.status = 404;
    throw error;
  }
  res.json(user);
}));

// Error-handling middleware (must come last)
app.use((err, req, res, next) => {
  console.error(`[${req.id || 'no-id'}]`, err);

  const status = err.status || 500;
  const response = {
    error: err.message || 'Internal Server Error',
  };

  if (process.env.NODE_ENV !== 'production') {
    response.stack = err.stack;
  }

  res.status(status).json(response);
});

This is one of the best examples of middleware in Express.js because it:

  • Works with async/await without littering routes with try/catch
  • Gives you a single place to control error format
  • Plays nicely with logging middleware (you can reuse req.id)

The pattern is still common in 2024–2025, especially in APIs that need consistent JSON error responses for frontend clients or mobile apps.


More real examples of middleware in Express.js you’ll see in production

Those 3 practical examples are the core. But real-world apps usually stack several more middleware functions. Here are additional examples of middleware in Express.js that are common in modern Node stacks.

Security headers with helmet

Security middleware is a low-effort, high-impact win. The helmet package sets common HTTP security headers that help mitigate attacks like XSS and clickjacking.

const helmet = require('helmet');

app.use(helmet());

You can customize it too:

app.use(helmet({
  contentSecurityPolicy: false, // turn off if you manage CSP separately
}));

The OWASP Foundation maintains guidance on secure headers that informs libraries like helmet. This is a simple example of middleware in Express.js that pays off immediately.

Rate limiting to protect APIs

With more public APIs and AI-driven scraping in 2024–2025, rate limiting is no longer optional for most public endpoints. A popular choice is express-rate-limit:

const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                 // limit each IP to 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
});

// Apply to all /api routes
app.use('/api', apiLimiter);

This is another clear example of middleware in Express.js: a function that inspects the request (IP, route), applies some logic, and either calls next() or blocks the request.

JSON body parsing and input validation

Body parsing used to be handled by body-parser, but Express now ships with express.json() and express.urlencoded() built in.

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

On top of that, many teams add validation middleware, often using libraries like joi or zod:

const Joi = require('joi');

function validateBody(schema) {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false,
      stripUnknown: true,
    });

    if (error) {
      return res.status(400).json({
        error: 'Validation failed',
        details: error.details.map((d) => d.message),
      });
    }

    req.body = value;
    next();
  };
}

const createUserSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
});

app.post('/users', validateBody(createUserSchema), asyncHandler(async (req, res) => {
  const user = await UserModel.create(req.body);
  res.status(201).json(user);
}));

Here you see another real example of middleware in Express.js: a reusable validator that runs before the route handler and either short-circuits with a 400 or normalizes the input.

For general guidance on input validation and security, the National Institute of Standards and Technology (NIST) provides security publications that many organizations reference when designing secure APIs.

Request timing and performance metrics

In 2024–2025, observability is a first-class concern. A lightweight custom middleware can record request duration and send it to a metrics system.

function timingMiddleware(req, res, next) {
  const start = process.hrtime.bigint();

  res.on('finish', () => {
    const end = process.hrtime.bigint();
    const durationMs = Number(end - start) / 1_000_000;

    console.log(JSON.stringify({
      path: req.path,
      method: req.method,
      status: res.statusCode,
      durationMs,
    }));

    // In a real app, send to Prometheus, StatsD, etc.
  });

  next();
}

app.use(timingMiddleware);

This pattern is an example of middleware in Express.js that hooks into the response lifecycle (res.on('finish')) to collect metrics without changing individual route handlers.

CORS middleware for frontend–backend integration

If you’re serving a React, Vue, or Next.js frontend from a different origin, you need CORS. The cors package wraps that logic in a clean middleware.

const cors = require('cors');

const corsOptions = {
  origin: ['https://example.com', 'https://admin.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true,
};

app.use(cors(corsOptions));

This is one of those quiet but important examples of middleware in Express.js that makes cross-origin requests behave without you thinking about headers on every route.


Putting it all together: composing multiple middleware functions

The real power of Express comes when you combine these patterns. A single route can use a chain of middleware to enforce security, validation, and business logic in a predictable order.

Here’s a realistic route that composes several examples of middleware in Express.js:

app.post(
  '/posts',
  cors(),                       // allow cross-origin if needed
  authMiddleware(),             // ensure user is authenticated
  validateBody(postSchema),     // validate input
  rateLimit({                   // per-route rate limit
    windowMs: 60 * 1000,
    max: 10,
  }),
  asyncHandler(async (req, res) => {
    const post = await PostModel.create({
      ...req.body,
      userId: req.user.id,
    });
    res.status(201).json(post);
  })
);

Each line is an example of middleware in Express.js doing one job:

  • CORS
  • Auth
  • Validation
  • Rate limiting
  • Async route logic with centralized error handling

This style keeps your code readable and makes it easy to swap in better implementations over time.


FAQ: Express.js middleware, with real examples

What are some common examples of middleware in Express.js?

Common examples include logging (like morgan), authentication middleware using JWT, error-handling middleware, security headers with helmet, rate limiting with express-rate-limit, body parsing with express.json(), validation middleware using joi or zod, and CORS middleware using the cors package.

Can you give an example of custom middleware in Express.js?

Yes. A simple example of custom middleware is a function that adds a timestamp to each request:

function timestampMiddleware(req, res, next) {
  req.requestTime = new Date().toISOString();
  next();
}

app.use(timestampMiddleware);

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

This example of middleware shows how you can enrich the req object and use that data later in your routes.

How do error-handling middleware examples in Express.js differ from normal middleware?

Error-handling middleware has four parameters: (err, req, res, next). Express uses that signature to recognize it as an error handler. Normal middleware has only three parameters: (req, res, next). You typically put error-handling middleware at the end of your middleware stack so it can catch errors from any previous handler.

Are third-party packages the only good examples of middleware in Express.js?

Not at all. Third-party packages like helmet, cors, and express-rate-limit are strong examples of middleware in Express.js, but many of the most valuable patterns in your app will be custom: your own auth logic, your own validation rules, your own logging format, and your own business-specific checks.

Where can I learn more about Express.js middleware patterns?

The official Express.js guide is still the best starting point. For broader Node and web security practices that influence many of these examples, the OWASP Cheat Sheet Series and NIST publications are widely referenced by engineering teams.

Explore More Node.js Code Snippets

Discover more examples and insights in this category.

View All Node.js Code Snippets