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
| Middleware | Purpose | Usage |
|---|---|---|
express.json() | Parse JSON request bodies -> req.body | app.use(express.json()) |
express.urlencoded({ extended }) | Parse URL-encoded form data -> req.body | app.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
| Package | Purpose | Install | Usage |
|---|---|---|---|
cors | Cross-origin resource sharing headers | npm i cors | app.use(cors()) |
helmet | Security HTTP headers (CSP, HSTS, etc.) | npm i helmet | app.use(helmet()) |
morgan | HTTP request logging | npm i morgan | app.use(morgan('dev')) |
express-rate-limit | Rate limiting per IP | npm i express-rate-limit | app.use(rateLimit({ windowMs, max })) |
cookie-parser | Parse cookies -> req.cookies | npm i cookie-parser | app.use(cookieParser()) |
compression | Gzip/Brotli response compression | npm i compression | app.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 Concept | What It Means |
|---|---|
| Same-origin | Same protocol + domain + port |
| Preflight | Browser sends OPTIONS request before non-simple requests (PUT, DELETE, custom headers) |
credentials: true | Allow cookies and auth headers across origins |
Never origin: '*' + credentials: true | CORS spec forbids this combination |
Middleware Types at a Glance
| Type | Registered With | Scope |
|---|---|---|
| Application-level | app.use(mw) | All requests (or path-filtered) |
| Router-level | router.use(mw) | Only routes on that router |
| Route-specific | app.get('/path', mw, handler) | Single route only |
| Error-handling | app.use((err, req, res, next) => {}) | Errors only (4 params) |
| Built-in | express.json(), etc. | Depends on where registered |
| Third-party | app.use(cors()), etc. | Depends on where registered |
| Custom | Your own functions | Depends 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
| Call | Effect |
|---|---|
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)
| Order | Middleware | Why Here |
|---|---|---|
| 1 | helmet() | Security headers on every response |
| 2 | cors() | CORS before any route handlers |
| 3 | rateLimit() | Reject abusive requests before parsing |
| 4 | express.json() | Parse body before routes |
| 5 | express.urlencoded() | Parse form data before routes |
| 6 | morgan() | Log after parsing |
| 7 | express.static() | Serve files without hitting routes |
| 8 | Custom enrichment | Request ID, timing |
| 9 | Route handlers | Application logic |
| 10 | 404 catch-all | After all routes |
| 11 | Error handler | Must be absolutely last |
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Missing next() | Request hangs | Add next() or send a response |
| Error handler with 3 params | Errors not caught | Add next as 4th parameter |
express.json() after routes | req.body is undefined | Move body parser before routes |
| 404 handler before routes | All requests return 404 | Move after all route definitions |
cors() after routes | CORS errors in browser | Move before route handlers |
| Async handler without catch | Unhandled promise rejection | Use 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 callnext(err).res.locals= pass data between middleware. Scoped to single request.
End of 3.6 quick revision.