Notifications

No notifications

/Phase 2

Middleware

Express Middleware

Middleware functions have access to the request, response, and next function. They execute in order and can modify req/res or end the cycle.

Middleware Flow

Request β†’ MW 1 β†’ MW 2 β†’ MW 3 β†’ Route Handler β†’ Response
          β”‚        β”‚        β”‚
          └──next()β”˜β”€β”€next()β”˜

Types of Middleware

TypeExamplePurpose
Built-inexpress.json()Parse JSON bodies
Third-partycors(), morgan()CORS, logging
CustomauthMiddlewareAuthentication
Error(err, req, res, next)Error handling
Route-levelrouter.use()Scoped to router

Middleware Signature

// Regular middleware: 3 params
function myMiddleware(req, res, next) {
  // Do something
  next(); // Pass to next
}

// Error middleware: 4 params (must have all 4!) function errorHandler(err, req, res, next) { res.status(500).json({ error: err.message }); }

On this page

Detailed Theory

What Middleware Actually Is

A middleware is a function with the signature (req, res, next) => { … } that runs *between* the request arriving and the route handler responding. Each one can:

  • Read or modify req (e.g. parse the body, attach req.user).
  • Read or modify res (e.g. add headers).
  • End the response (res.json(…)) and stop the chain.
  • Pass control on by calling next().
  • Hand off to the error handler with next(err).
That's it. Express is, at its core, *a list of middlewares*.

Your First Middleware

function logger(req, res, next) {
  console.log(${req.method} ${req.url});
  next(); // hand off to the next middleware / route
}

app.use(logger);

Every request now logs before doing anything else.

Built-in & Common Middlewares

app.use(express.json());                 // parse JSON bodies
app.use(express.urlencoded({ extended: true })); // parse form posts
app.use(express.static('public'));       // serve files from /public
app.use(cors());                         // CORS headers
app.use(helmet());                       // security headers
app.use(morgan('dev'));                  // request logger
app.use(cookieParser());                 // parse cookies

Most "how do I do X in Express" questions resolve to "add this middleware".

Three Ways to Mount Middleware

// 1. Globally β€” runs for every request
app.use(logger);

// 2. Path-scoped β€” only for URLs under /api/admin app.use('/api/admin', requireAdmin);

// 3. Per-route β€” just for this endpoint app.get('/profile', requireAuth, (req, res) => res.json(req.user));

You can chain as many as you want before the final handler:

app.post('/posts', requireAuth, validatePost, createPost);

Beginner Mistakes to Skip

1. Forgetting next(). The request hangs forever β€” client times out. Either call next() *or* end the response. 2. Calling next() *and* res.send(…). The next middleware tries to send again β†’ Cannot set headers after they are sent. 3. Wrong order. app.use(routes); app.use(express.json()); β†’ routes see req.body === undefined. 4. Error handler with 3 args. Express only treats it as an error handler if it has four parameters (err, req, res, next). 5. Async errors not forwarded. throw inside an async middleware in Express 4 won't reach your error handler unless you try/catch and call next(err).

Intermediate: Order Is Everything

app.use(helmet());            // 1. security headers first
app.use(cors());              // 2. CORS
app.use(express.json());      // 3. parse body
app.use(morgan('dev'));       // 4. log
app.use(rateLimit(...));      // 5. throttle
app.use(authenticate);        // 6. set req.user (does NOT block anonymous)
app.use('/api', apiRoutes);   // 7. real routes
app.use(notFound);            // 8. 404 β€” only reached if nothing matched
app.use(errorHandler);        // 9. errors LAST

A middleware can only protect what comes after it.

Intermediate: Conditional / Skipping Middleware

function maybeAuth(req, res, next) {
  if (req.path.startsWith('/public')) return next(); // skip
  return requireAuth(req, res, next);
}

Or use app.use('/private', requireAuth) so it never runs on the public branch in the first place β€” cleaner.

Intermediate: Middleware Factories

A function that returns a middleware lets you parametrise behaviour:

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

app.delete('/posts/:id', requireAuth, requireRole('admin'), deletePost);

A simple in-memory rate limiter:

function rateLimit(max, windowMs) {
  const hits = new Map();
  return (req, res, next) => {
    const now = Date.now(), key = req.ip;
    const list = (hits.get(key) || []).filter(t => t > now - windowMs);
    if (list.length >= max) return res.status(429).json({ error: 'slow down' });
    list.push(now); hits.set(key, list); next();
  };
}
app.use(rateLimit(100, 60_000)); // 100 req/min/IP

(For real apps use express-rate-limit β€” it handles distributed stores, headers, etc.)

Intermediate: Async Middleware

// Wrapper that forwards rejections to next()
const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

const requireAuth = asyncH(async (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'no token' }); req.user = await verifyJwt(token); next(); });

Express 5 (current stable) catches thrown async errors automatically; in v4 the wrapper above is the standard idiom.

Intermediate: The Error-Handling Middleware

Four parameters tell Express "I'm an error handler":

app.use((err, req, res, next) => {
  // your custom AppError class can carry .statusCode and .code
  const status = err.statusCode || 500;
  res.status(status).json({
    error: { code: err.code || 'internal', message: err.message },
    ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
  });
});

Anything that calls next(err) lands here. Always last.

Intermediate: req Is a Suitcase

Middlewares pile context onto req so later code doesn't need to redo work:

requireAuth   β†’ sets req.user
requestId     β†’ sets req.id (for log correlation)
locale        β†’ sets req.locale
validate(...) β†’ sets req.validated

By the time the route handler runs, everything it needs is already on req.

Advanced: Sub-Apps and Routers as Middleware

A Router is *itself* middleware:

import adminRouter from './routes/admin.js';
app.use('/admin', requireAdmin, adminRouter);

You can compose middleware stacks per-feature, mount them, and even nest routers many levels deep.

Advanced: Streaming & Compression Pitfalls

Middlewares like compression() wrap res.write/res.end. If you also write the response *manually as a stream*, make sure compression sits before the route, and don't pre-set Content-Length when streaming β€” chunked encoding will sort itself out.

Advanced: Performance Budget

Every middleware costs a few Β΅s and a function call. Tips:

  • Mount expensive ones only on paths that need them (e.g. file upload parser only on /upload).
  • Prefer the official express-* middlewares β€” they're battle-tested for hot paths.
  • Avoid synchronous CPU work; never fs.readFileSync per request.

Advanced: Logging Pattern

app.use((req, res, next) => {
  req.id = crypto.randomUUID();
  req.start = process.hrtime.bigint();
  res.on('finish', () => {
    const ms = Number(process.hrtime.bigint() - req.start) / 1e6;
    log.info({ id: req.id, m: req.method, u: req.url, s: res.statusCode, ms });
  });
  next();
});

A single req.id lets you correlate logs across DB calls, downstream APIs, and error traces.

Practice Path

1. Write a logger middleware that prints METHOD URL β†’ status (ms) for every request (use res.on('finish')). 2. Write requireAuth that fakes auth: 401 unless req.headers['x-user'] exists; otherwise sets req.user = { name: req.headers['x-user'] }. Mount it on /api. 3. Build a requireRole('admin') factory and protect a DELETE /api/posts/:id route with it. 4. Add a 4-arg error handler that maps a custom HttpError class with .statusCode to the right HTTP status.