Best examples of adding middleware in a Node.js API – Practical Examples for 2025

If you’re building APIs in Node, you can’t avoid middleware – and honestly, you shouldn’t want to. Middleware is where you plug in logging, auth, rate limiting, validation, and all the little guardrails that keep your API from spiraling into chaos. In this guide, we’ll walk through real, modern examples of adding middleware in a Node.js API – practical examples you can drop straight into your codebase. We’ll stay out of theory-land and focus on how teams actually wire this up in production: from basic logging to JWT auth, from security headers to request validation and error handling. Along the way, you’ll see multiple examples of adding middleware in a Node.js API – practical examples using Express-style syntax and patterns that work just as well with frameworks like Fastify or NestJS. If you’re tired of vague explanations and want concrete, 2025-ready patterns, this is the guide you bookmark and reuse.
Written by
Jamie
Published

Real-world examples of adding middleware in a Node.js API – practical examples

Let’s start where most developers actually start: taking a plain Express server and layering in middleware until it feels like a real API instead of a weekend experiment.

Here’s a minimal Express setup we’ll keep extending:

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

app.use(express.json()); // built-in middleware

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

app.listen(3000, () => {
  console.log('API listening on http://localhost:3000');
});

From here, we’ll walk through several examples of adding middleware in a Node.js API – practical examples that mirror what you’d see in a production backend.


Logging and request tracing – the first middleware you actually feel

If you’re looking for a simple example of adding middleware in a Node.js API, request logging is the classic starting point. You want to know who hit what endpoint, when, and with which status code.

A lightweight, no-dependency logger middleware might look like this:

function requestLogger(req, res, next) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(
      `\({req.method} }\(req.originalUrl} \({res.statusCode} - }\(duration}ms`
    );
  });

  next();
}

app.use(requestLogger);

Real examples in production often pair this with a correlation ID so you can trace a single request across services.

const { randomUUID } = require('crypto');

function correlationId(req, res, next) {
  const id = req.headers['x-correlation-id'] || randomUUID();
  req.correlationId = id;
  res.setHeader('X-Correlation-Id', id);
  next();
}

app.use(correlationId);

These two together are among the best examples of middleware that pay off instantly: debugging, performance tuning, and observability all become easier.


Security and headers – examples include helmet, CORS, and rate limiting

Modern APIs are expected to ship with sane security defaults. Middleware is where you put them.

Security headers with helmet

One of the most common examples of adding middleware in a Node.js API – practical examples you’ll see on GitHub – is using helmet:

const helmet = require('helmet');

app.use(helmet());

This adds HTTP headers that help protect against common attacks like XSS or clickjacking. While you should always pair this with secure coding practices and testing, it’s a low-friction upgrade.

For security guidance and why these headers matter, the OWASP organization maintains a useful overview of HTTP security controls: https://owasp.org/www-project-secure-headers/.

CORS middleware

If your frontend is on a different domain than your API, you need Cross-Origin Resource Sharing (CORS) configured.

const cors = require('cors');

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

app.use(cors(corsOptions));

This is another everyday example of adding middleware in a Node.js API: you centralize cross-origin logic instead of sprinkling headers across routes.

Rate limiting

To keep bots or misbehaving clients from hammering your endpoints, you add a rate limiter. A common middleware pattern uses 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,    // RateLimit-* headers
  legacyHeaders: false
});

app.use('/api', apiLimiter);

Even in 2025, this remains one of the best examples of a simple mitigation against basic abuse. For more on API security best practices, the NIST publications library is a solid reference point, especially when you’re operating in regulated environments.


Authentication and authorization – JWT middleware in action

Authentication middleware is where “toy API” turns into “real application.” Here’s a straightforward JWT auth example of adding middleware in a Node.js API.

const jwt = require('jsonwebtoken');

function authMiddleware(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

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

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid token' });
    }

    req.user = user; // attach decoded payload
    next();
  });
}

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

Here, authMiddleware is a textbook example of route-level middleware: you attach it only where needed instead of globally.

You can layer authorization on top of this with a role-checking middleware:

function requireRole(role) {
  return function (req, res, next) {
    if (!req.user || req.user.role !== role) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

app.delete('/admin/users/:id', authMiddleware, requireRole('admin'), (req, res) => {
  // delete user logic
  res.status(204).send();
});

These two together are real examples of adding middleware in a Node.js API that map directly to production-grade security requirements.


Validation middleware – keeping garbage out of your database

By 2025, most serious Node APIs use some form of schema-based validation. Middleware is where you enforce those schemas.

Here’s an example using zod, but the pattern is similar for Joi, Yup, or built-in framework validators:

const { z } = require('zod');

const createUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(1)
});

function validateBody(schema) {
  return function (req, res, next) {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        error: 'Validation failed',
        details: result.error.issues
      });
    }
    req.body = result.data; // sanitized
    next();
  };
}

app.post('/users', validateBody(createUserSchema), (req, res) => {
  // safe to use req.body here
  res.status(201).json({ user: req.body });
});

This is one of the best examples of adding middleware in a Node.js API because it centralizes validation logic and keeps route handlers focused on business rules, not input checking.

For broader data validation concepts and error handling patterns, universities like MIT publish coursework and open materials that are helpful when you want to go deeper on software design and reliability.


Error-handling middleware – the safety net

In Express-style frameworks, error-handling middleware has a distinct signature with four arguments. It’s your last line of defense before an exception becomes a 500 stack trace in someone’s browser.

Here’s a simple pattern:

function errorHandler(err, req, res, next) {
  console.error('Error:', err);

  const status = err.status || 500;
  const message =
    status === 500 ? 'Internal server error' : err.message || 'Error';

  res.status(status).json({ error: message });
}

app.use(errorHandler);

You can pair this with a small helper that throws HTTP-style errors from your route handlers:

class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.status = status;
  }
}

app.get('/items/:id', async (req, res, next) => {
  try {
    const item = await findItemById(req.params.id);
    if (!item) throw new HttpError(404, 'Item not found');
    res.json(item);
  } catch (err) {
    next(err); // forwarded to errorHandler
  }
});

This pattern is one of the cleanest examples of adding middleware in a Node.js API to standardize how your API talks to clients about failures.


Performance and caching middleware – getting faster without rewriting everything

Not every performance problem needs a full rewrite. Sometimes you just need smart middleware.

Simple in-memory cache

Here’s a basic cache middleware for endpoints where data doesn’t change constantly:

const cache = new Map();

function cacheMiddleware(ttlMs) {
  return function (req, res, next) {
    const key = req.originalUrl;
    const cached = cache.get(key);

    if (cached && cached.expires > Date.now()) {
      return res.json(cached.data);
    }

    const originalJson = res.json.bind(res);

    res.json = (body) => {
      cache.set(key, {
        data: body,
        expires: Date.now() + ttlMs
      });
      return originalJson(body);
    };

    next();
  };
}

app.get('/public/stats', cacheMiddleware(60 * 1000), async (req, res) => {
  const stats = await computeExpensiveStats();
  res.json(stats);
});

This is a practical example of adding middleware in a Node.js API to improve perceived speed without touching the core logic.

Response compression

Another low-effort win: gzip or Brotli compression using compression middleware.

const compression = require('compression');

app.use(compression());

You don’t need to micromanage it; the middleware negotiates with the client based on Accept-Encoding headers.


The Node ecosystem in 2024–2025 has matured past the “everything in one file” era. The best examples of production APIs tend to share a few patterns:

Grouped middleware stacks

Instead of a single giant app.use section, teams group middleware by responsibility:

// security.js
module.exports = function applySecurity(app) {
  app.use(helmet());
  app.use(cors(corsOptions));
  app.use(apiLimiter);
};

// logging.js
module.exports = function applyLogging(app) {
  app.use(correlationId);
  app.use(requestLogger);
};

// app.js
applyLogging(app);
applySecurity(app);
app.use(express.json());
app.use('/api', apiRouter);
app.use(errorHandler);

This doesn’t change how middleware works, but it’s a real example of adding middleware in a Node.js API in a way that scales as your project grows.

Framework-agnostic thinking

Even if you’re using NestJS, Fastify, or Hapi, the same concepts apply:

  • A function runs before or after your handler.
  • It can read/modify the request.
  • It can short-circuit the response.

Once you understand these examples of adding middleware in a Node.js API with Express, translating them to decorators in NestJS or hooks in Fastify is mostly syntax.


FAQ: common questions about middleware in Node.js APIs

What are some real examples of middleware in a Node.js API?

Real-world examples include:

  • Request logging and correlation ID middleware for debugging
  • Authentication and authorization middleware using JWTs or sessions
  • Security middleware like helmet, CORS configuration, and rate limiting
  • Validation middleware that checks request bodies against schemas
  • Error-handling middleware that converts exceptions into clean JSON responses
  • Performance middleware such as caching and response compression

All of the code snippets above are practical examples of adding middleware in a Node.js API that you can adapt directly.

Can I apply middleware only to specific routes?

Yes. In Express-style frameworks you can attach middleware at the route level:

app.get('/private', authMiddleware, (req, res) => {
  res.json({ secret: '42' });
});

Or at the router level:

const adminRouter = express.Router();

adminRouter.use(authMiddleware, requireRole('admin'));

adminRouter.get('/dashboard', (req, res) => {
  res.json({ stats: 'admin stuff' });
});

app.use('/admin', adminRouter);

These patterns are some of the best examples of keeping your middleware usage organized and predictable.

How many middleware functions is too many?

There’s no hard number, but you’ll feel it when every request passes through a long chain of logic that’s hard to reason about. A reasonable rule of thumb in 2025 backend teams is:

  • Keep global middleware focused on cross-cutting concerns (logging, security, JSON parsing).
  • Use route-level middleware for things that are context-specific (auth, validation, feature flags).

If you find yourself stacking five or six middlewares on every route, consider grouping them into a single higher-level function.

Are these examples of middleware compatible with serverless (AWS Lambda, etc.)?

Mostly yes, with some adaptation. Libraries like serverless-http let you wrap an Express app and run it in Lambda, Azure Functions, or similar platforms. Your existing middleware (logging, auth, validation, error handling) generally works without major changes. The main caveats are around connection reuse, cold starts, and some request/response object differences.

Where can I learn more about secure API design beyond middleware?

Middleware is only one layer of API security. For deeper background on authentication, encryption, and secure software practices, look at:

  • NIST’s cybersecurity publications: https://csrc.nist.gov/publications
  • OWASP’s API Security Top 10: https://owasp.org/www-project-api-security/
  • University security courses and open materials, such as those linked from https://web.mit.edu/

These resources go far beyond examples of adding middleware in a Node.js API, but they give you the broader context for why those middleware patterns matter.

Explore More Building a Simple API with Node.js

Discover more examples and insights in this category.

View All Building a Simple API with Node.js