Episode 3 — NodeJS MongoDB Backend Architecture / 3.6 — Middleware in Express

3.6 — Middleware in Express: Quick Revision

Episode 3 supplement -- print-friendly cheat sheet.

How to use

Skim -> drill weak spots in 3.6.a through 3.6.f -> 3.6-Exercise-Questions.md -> 3.6-Interview-Questions.md.


Middleware Signature

// Standard middleware (3 params)
(req, res, next) => {
  // modify req/res, or send response, or call next()
  next();
}

// Error-handling middleware (4 params -- Express checks function.length)
(err, req, res, next) => {
  res.status(err.statusCode || 500).json({ error: err.message });
}

Rule: Every middleware MUST either call next() or send a response. If it does neither, the request hangs.


Middleware Pipeline Diagram

Client Request
      |
      v
+---------------------------+
| 1. helmet()               |  Set security headers
+---------------------------+
      |  next()
      v
+---------------------------+
| 2. cors()                 |  Add CORS headers
+---------------------------+
      |  next()
      v
+---------------------------+
| 3. rateLimit()            |  Block if limit exceeded
+---------------------------+
      |  next()
      v
+---------------------------+
| 4. express.json()         |  Parse JSON body -> req.body
+---------------------------+
      |  next()
      v
+---------------------------+
| 5. morgan('dev')          |  Log request
+---------------------------+
      |  next()
      v
+---------------------------+
| 6. authMiddleware         |  Verify token -> req.user
+---------------------------+
      |  next()
      v
+---------------------------+
| 7. Route Handler          |  Business logic -> res.json()
+---------------------------+
      |
      v  (if next(error) called)
+---------------------------+
| 8. Error Handler          |  Format error -> res.json()
+---------------------------+
      |
      v
Client Response

Built-in Middleware Table

MiddlewarePurposeUsage
express.json()Parse JSON request bodies -> req.bodyapp.use(express.json())
express.urlencoded({ extended })Parse URL-encoded form data -> req.bodyapp.use(express.urlencoded({ extended: true }))
express.static(root)Serve static files (CSS, JS, images)app.use(express.static('public'))

That is it -- everything else is third-party or custom.


Third-Party Middleware Table

PackagePurposeInstallUsage
corsCross-origin resource sharing headersnpm i corsapp.use(cors())
helmetSecurity HTTP headers (CSP, HSTS, etc.)npm i helmetapp.use(helmet())
morganHTTP request loggingnpm i morganapp.use(morgan('dev'))
express-rate-limitRate limiting per IPnpm i express-rate-limitapp.use(rateLimit({ windowMs, max }))
cookie-parserParse cookies -> req.cookiesnpm i cookie-parserapp.use(cookieParser())
compressionGzip/Brotli response compressionnpm i compressionapp.use(compression())

Error Handler Pattern

// Custom error class
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = statusCode >= 500 ? 'error' : 'fail';
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

// Throwing errors in routes/services
if (!user) throw new AppError('User not found', 404);

// Forwarding errors in middleware
app.get('/api/users/:id', async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) return next(new AppError('Not found', 404));
    res.json(user);
  } catch (err) {
    next(err);
  }
});

// Global error handler (MUST be last, MUST have 4 params)
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    status: err.status || 'error',
    message: err.isOperational ? err.message : 'Something went wrong',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
});

Async Handler (Express 4 Fix)

// Wraps async functions so rejected promises call next(err)
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Usage -- no try/catch needed
app.get('/api/users', asyncHandler(async (req, res) => {
  const users = await User.find();
  res.json(users);
}));

Express 5 handles this automatically. Until then, use asyncHandler.


CORS Configuration

const cors = require('cors');

// Development: allow all origins
app.use(cors());

// Production: whitelist specific origins
app.use(cors({
  origin: ['https://myapp.com', 'https://admin.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true    // allow cookies/auth headers
}));
CORS ConceptWhat It Means
Same-originSame protocol + domain + port
PreflightBrowser sends OPTIONS request before non-simple requests (PUT, DELETE, custom headers)
credentials: trueAllow cookies and auth headers across origins
Never origin: '*' + credentials: trueCORS spec forbids this combination

Middleware Types at a Glance

TypeRegistered WithScope
Application-levelapp.use(mw)All requests (or path-filtered)
Router-levelrouter.use(mw)Only routes on that router
Route-specificapp.get('/path', mw, handler)Single route only
Error-handlingapp.use((err, req, res, next) => {})Errors only (4 params)
Built-inexpress.json(), etc.Depends on where registered
Third-partyapp.use(cors()), etc.Depends on where registered
CustomYour own functionsDepends on where registered

Middleware Factory Pattern

// Factory: accepts config, returns middleware
const requireRole = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
};

// Usage
app.delete('/api/users/:id', requireRole('admin'), deleteUser);
app.get('/api/reports', requireRole('admin', 'manager'), getReports);

next() Variants

CallEffect
next()Pass to next middleware in stack
next(err)Skip all normal middleware, jump to error handler
next('route')Skip remaining handlers on current route, jump to next route match

Router-Level Middleware

// routes/adminRoutes.js
const router = express.Router();

// Middleware only for this router
router.use(requireAuth);
router.use(requireRole('admin'));

router.get('/dashboard', getDashboard);
router.get('/users', listUsers);
router.delete('/users/:id', deleteUser);

module.exports = router;

// app.js -- mount at prefix
app.use('/api/admin', require('./routes/adminRoutes'));

{ mergeParams: true } lets a child router access req.params from its parent router.


Production Middleware Stack (Recommended Order)

OrderMiddlewareWhy Here
1helmet()Security headers on every response
2cors()CORS before any route handlers
3rateLimit()Reject abusive requests before parsing
4express.json()Parse body before routes
5express.urlencoded()Parse form data before routes
6morgan()Log after parsing
7express.static()Serve files without hitting routes
8Custom enrichmentRequest ID, timing
9Route handlersApplication logic
10404 catch-allAfter all routes
11Error handlerMust be absolutely last

Common Mistakes

MistakeSymptomFix
Missing next()Request hangsAdd next() or send a response
Error handler with 3 paramsErrors not caughtAdd next as 4th parameter
express.json() after routesreq.body is undefinedMove body parser before routes
404 handler before routesAll requests return 404Move after all route definitions
cors() after routesCORS errors in browserMove before route handlers
Async handler without catchUnhandled promise rejectionUse asyncHandler wrapper

One-Liners

  • Middleware = function with (req, res, next). Modify, respond, or pass along.
  • Order matters = middleware runs top to bottom. Body parser before routes. Error handler last.
  • next(err) = skip everything, jump to error handler.
  • 4 params = error handler. 3 params = regular middleware. Express checks function.length.
  • helmet = security headers. cors = cross-origin access. morgan = logging.
  • Factory pattern = (config) => (req, res, next) => {}. Reusable + configurable.
  • asyncHandler = wrap async middleware so rejected promises call next(err).
  • res.locals = pass data between middleware. Scoped to single request.

End of 3.6 quick revision.