TL;DR: Middleware is a function that runs between a request arriving at your server and a response going back to the client. It can check authentication, parse JSON bodies, log requests, block bad actors, or add security headers. In Express, middleware calls next() to pass control to the next function in the chain.

Why AI Coders Need to Know This

Almost every backend AI generates for you will include middleware. When Cursor or Claude scaffolds an Express app, the first 20 lines of server.js will be app.use() calls. When you build a Next.js app with protected routes, it will generate a middleware.ts file. If you don't understand what middleware is and how it works, you won't know what to keep, what to configure, or why your API keeps returning 401 Unauthorized.

Middleware is one of those concepts where the name actually makes sense once you understand it: it's code that sits in the middle — between the request coming in and the response going out. Once you have that mental model, everything else follows naturally.

The Mental Model: A Request Assembly Line

Think of an HTTP request arriving at your server as a package on an assembly line. Each middleware function is a station the package passes through before reaching its destination (the route handler). At each station, a worker can:

  • Inspect the package — read the headers, check authentication tokens, log the request
  • Modify the package — parse the JSON body, add user data from a database lookup
  • Reject the package — send a 401 or 403 response without passing it further
  • Pass it on — call next() to send it to the next station

The route handler at the end of the chain is the final destination — it produces the actual response. Middleware functions are everything that runs before it gets there.

Real Scenario

Prompt I Would Type

Build me an Express.js REST API for a task management app.
Include:
- User authentication with JWT
- Protected routes only logged-in users can access
- Request logging
- Proper security headers
- CORS so my React frontend can call it

This prompt will generate a file with at least five middleware functions applied before any of your actual routes. Here's what that looks like and what each piece does.

What AI Generated

const express = require('express');
const cors = require('cors');           // handles Cross-Origin Resource Sharing
const helmet = require('helmet');       // adds security headers automatically
const jwt = require('jsonwebtoken');
const morgan = require('morgan');       // HTTP request logger

const app = express();

// ── GLOBAL MIDDLEWARE ──────────────────────────────────────────────
// These run on EVERY request, in the order they're registered

app.use(helmet());
// Adds ~15 security headers automatically (X-Frame-Options, CSP, etc.)
// Without it, browsers and scanners flag your API as insecure

app.use(cors({
  origin: process.env.ALLOWED_ORIGIN,  // only allow your frontend's domain
  credentials: true                    // allow cookies to be sent cross-origin
}));
// Without cors(), your React app will get "blocked by CORS policy" errors

app.use(express.json());
// Parses incoming JSON request bodies into req.body
// Without this, req.body is always undefined

app.use(morgan('dev'));
// Logs every request: GET /api/tasks 200 12ms
// Essential for debugging — you can see every request hitting your server

// ── CUSTOM AUTH MIDDLEWARE ─────────────────────────────────────────
// This runs only on routes you apply it to

function requireAuth(req, res, next) {
  // 1. Get the token from the Authorization header
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
    // Note: return is important — stops execution so next() isn't called
  }

  const token = authHeader.split(' ')[1];  // extract the token after "Bearer "

  try {
    // 2. Verify the token using our secret key
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // 3. Attach user data to the request object for downstream handlers to use
    req.user = decoded;

    // 4. Pass control to the next middleware or route handler
    next();
  } catch (err) {
    // Token is invalid or expired
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// ── ROUTES ────────────────────────────────────────────────────────
// Public route — no auth required
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

// Protected route — requireAuth runs first, then the handler
app.get('/api/tasks', requireAuth, async (req, res) => {
  // req.user is available here because requireAuth put it there
  const tasks = await db.getTasks(req.user.userId);
  res.json(tasks);
});

// ── ERROR HANDLING MIDDLEWARE ──────────────────────────────────────
// Must be registered LAST — 4 parameters is what makes Express treat it as error middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong' });
});

Understanding Each Part

app.use() — Registering Middleware

app.use(fn) registers a middleware function to run on every incoming request. app.use('/api', fn) runs it only on paths starting with /api. Order matters — middleware runs in the order it's registered. If you put your auth middleware before your routes, it protects those routes. If you put it after, it never runs.

The (req, res, next) Signature

Every Express middleware function takes three parameters:

  • req — the request object (headers, body, params, URL, etc.)
  • res — the response object (send a response back to the client)
  • next — a function that passes control to the next middleware

If you want the request to continue: call next(). If you want to stop and send a response: call res.json() or similar (and return so nothing else runs). If there's an error: call next(err).

helmet() — Security Headers in One Line

Helmet is a package that sets about 15 HTTP security headers automatically — things like X-Frame-Options (prevents clickjacking), X-Content-Type-Options (prevents MIME sniffing), and a basic Content Security Policy. It's one line of code that handles a whole category of web security vulnerabilities. Always include it.

cors() — Cross-Origin Requests

By default, browsers block JavaScript on one domain from making requests to another domain. Your React app on localhost:3000 can't call your API on localhost:4000 without CORS headers. The cors() middleware adds the necessary headers to allow this. Always configure it with a specific origin — not a wildcard * — in production.

express.json() — Parsing Request Bodies

When a client sends a POST request with JSON data, the raw request body is a string. express.json() parses that string and makes it available as req.body. Without it, every POST handler would have to parse the body manually. This is so commonly forgotten that "req.body is undefined" is one of the most common Express beginner questions.

Next.js Edge Middleware

In Next.js, middleware works differently — it runs at the Edge (CDN layer) before the page loads, and lives in a middleware.ts file at the project root. You return NextResponse.next() to continue, NextResponse.redirect() to redirect, or NextResponse.rewrite() to serve different content:

// middleware.ts — runs before EVERY request in your Next.js app
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token');

  // If no token and trying to access a protected route, redirect to login
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();  // continue to the requested page
}

// Only run this middleware on dashboard routes
export const config = {
  matcher: ['/dashboard/:path*'],
};

What AI Gets Wrong About Middleware

1. Wildcard CORS Origins

AI frequently generates cors({ origin: '*' }) — allowing any domain to call your API. This is fine for public APIs but dangerous for APIs that handle authentication or user data. Set origin to your specific frontend domain.

2. Missing the return Before Sending a Response

If you send a response in middleware without returning, execution continues and Node.js will try to send a second response — causing the infamous "Cannot set headers after they are sent" error. Always use return res.json(...) when short-circuiting middleware.

3. Error Middleware with Wrong Parameter Count

Express identifies error-handling middleware by the four-parameter signature (err, req, res, next). If you write only three parameters, Express treats it as regular middleware and your errors won't be caught. This is a subtle gotcha that AI sometimes gets wrong.

4. Middleware Order Issues

AI sometimes registers route handlers before the middleware that should protect them. If app.get('/api/admin', handler) appears before app.use(requireAuth), the auth check never runs for that route. Global middleware must be registered before the routes it should protect.

How to Debug Middleware with AI

In Cursor

When a request hits an unexpected 401 or the body is undefined, open your server file and ask Cursor: "Trace what happens to a POST request to /api/tasks — list every middleware function that runs in order and what each one does to req." This gives you a mental model of the execution flow.

In Windsurf

Use Cascade to find all middleware registrations across the codebase: "List every app.use() and middleware function in this Express app in the order they execute, and identify any that might cause the 401 error I'm seeing."

In Claude Code

Paste your middleware function and ask: "This auth middleware is returning 401 even when I include a valid token. Review the token extraction, verification, and next() call logic." Claude excels at spotting the off-by-one errors and missing return statements that commonly break middleware.

The console.log Trick

Add a log at the start and end of each middleware to trace execution: console.log('AUTH MIDDLEWARE START') and console.log('AUTH MIDDLEWARE — calling next()'). This immediately shows you which middleware is running, which is short-circuiting, and where the chain breaks.

What to Learn Next

Frequently Asked Questions

Middleware is a function that runs between a request arriving at your server and a response being sent back. It can inspect, modify, or reject requests and responses. Common uses include authentication checks, logging, rate limiting, request parsing, and error handling.

In Express, calling next() passes control to the next middleware function in the chain. If you don't call next(), the request is stuck and the client never receives a response. If you call next(error), Express skips to the error-handling middleware.

Next.js middleware runs at the Edge (before the page or API route loads) and can redirect, rewrite, or modify requests. It's defined in a middleware.ts file at the project root and commonly used for authentication checks, A/B testing, and locale redirects.

A route handler responds to a specific URL path and HTTP method — it sends the final response. Middleware runs before the route handler (or in between handlers) and typically inspects or transforms the request without sending a final response, then calls next() to continue the chain.

When AI generates an Express or Next.js backend, it often adds middleware for security (helmet, cors), body parsing (express.json()), authentication (JWT verification), and logging. These are best practices. Understanding each piece helps you configure them correctly and remove ones your app doesn't need.