Episode 3 — NodeJS MongoDB Backend Architecture / 3.6 — Middleware in Express
3.6.b — Types of Middleware
In one sentence: Express middleware falls into three categories -- built-in middleware shipped with Express, third-party middleware installed via npm, and custom middleware you write yourself -- each serving different needs in the request pipeline.
Navigation: <- 3.6.a Understanding Middleware | 3.6.c -- Application-Level Middleware ->
1. The Three Categories
| Category | Source | Examples |
|---|---|---|
| Built-in | Ships with Express (no extra install) | express.json(), express.urlencoded(), express.static() |
| Third-party | Installed via npm install | cors, morgan, helmet, cookie-parser, compression, express-rate-limit |
| Custom | Written by you | Request loggers, auth guards, validators, timing middleware |
2. Built-in Middleware
Express 4.x+ includes three middleware functions out of the box.
2.1 express.json() -- Parse JSON Request Bodies
Parses incoming requests where Content-Type: application/json and makes the parsed data available on req.body.
const express = require('express');
const app = express();
// Enable JSON body parsing
app.use(express.json());
app.post('/users', (req, res) => {
console.log(req.body); // { name: "Alice", age: 30 }
res.status(201).json({ message: 'User created', user: req.body });
});
app.listen(3000);
Configuration options:
app.use(express.json({
limit: '10kb', // Max body size (default: '100kb')
strict: true, // Only accept arrays and objects (default: true)
type: 'application/json' // Content-Type to parse (default)
}));
Without express.json(): req.body is undefined for JSON POST/PUT/PATCH requests.
2.2 express.urlencoded() -- Parse Form Data
Parses incoming requests where Content-Type: application/x-www-form-urlencoded (standard HTML form submissions).
// Enable URL-encoded form data parsing
app.use(express.urlencoded({ extended: true }));
app.post('/login', (req, res) => {
console.log(req.body); // { username: "alice", password: "secret" }
res.send(`Welcome, ${req.body.username}!`);
});
The extended option:
| Value | Parser Used | Supports |
|---|---|---|
true | qs library | Rich objects and arrays (user[name]=Alice) |
false | querystring module | Simple key-value pairs only |
Best practice: Use extended: true unless you have a reason not to.
2.3 express.static() -- Serve Static Files
Serves files (HTML, CSS, JS, images) from a directory.
// Serve files from the "public" folder
app.use(express.static('public'));
// Now:
// GET /style.css --> serves public/style.css
// GET /app.js --> serves public/app.js
// GET /images/logo.png --> serves public/images/logo.png
With a URL prefix:
// Files served under /static/ prefix
app.use('/static', express.static('public'));
// GET /static/style.css --> serves public/style.css
Configuration options:
const path = require('path');
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: '1d', // Browser cache duration
index: 'index.html', // Default file (default: 'index.html')
dotfiles: 'ignore', // How to handle dotfiles (default: 'ignore')
etag: true, // Enable ETag generation (default: true)
extensions: ['html'] // Try these extensions if file not found
}));
2.4 Built-in Middleware Summary
const express = require('express');
const app = express();
// Standard setup -- these three cover most basic needs
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse form submissions
app.use(express.static('public')); // Serve static files
// Routes go here...
app.listen(3000);
3. Third-Party Middleware
Installed via npm. Each solves a specific cross-cutting concern.
3.1 cors -- Cross-Origin Resource Sharing
Enables requests from different origins (e.g., frontend on localhost:5173 calling API on localhost:3000).
npm install cors
const cors = require('cors');
// Allow all origins (development)
app.use(cors());
// Allow specific origins (production)
app.use(cors({
origin: 'https://myapp.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // Allow cookies to be sent
}));
// Multiple origins
app.use(cors({
origin: ['https://myapp.com', 'https://admin.myapp.com'],
}));
3.2 morgan -- HTTP Request Logger
Logs every HTTP request to the console with useful info.
npm install morgan
const morgan = require('morgan');
// Predefined formats
app.use(morgan('dev')); // Colored, concise -- great for development
// Output: GET /api/users 200 12.345 ms - 58
app.use(morgan('combined')); // Apache-style -- great for production logs
// Output: ::1 - - [10/Oct/2025:13:55:36 +0000] "GET /api/users HTTP/1.1" 200 58
app.use(morgan('tiny')); // Minimal
// Output: GET /api/users 200 58 - 12.345 ms
Custom format:
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
Log to a file:
const fs = require('fs');
const path = require('path');
const accessLogStream = fs.createWriteStream(
path.join(__dirname, 'access.log'),
{ flags: 'a' } // Append mode
);
app.use(morgan('combined', { stream: accessLogStream }));
3.3 helmet -- Security Headers
Sets various HTTP headers to protect against common attacks.
npm install helmet
const helmet = require('helmet');
// Enable all default protections
app.use(helmet());
Headers that helmet sets:
| Header | Protection |
|---|---|
Content-Security-Policy | Prevents XSS by controlling resource loading |
X-Content-Type-Options: nosniff | Stops browsers from MIME-sniffing |
X-Frame-Options: SAMEORIGIN | Prevents clickjacking |
Strict-Transport-Security | Forces HTTPS |
X-XSS-Protection: 0 | Disables buggy browser XSS filter |
X-DNS-Prefetch-Control: off | Controls DNS prefetching |
X-Permitted-Cross-Domain-Policies | Restricts Adobe product policies |
Custom configuration:
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
crossOriginEmbedderPolicy: false, // Disable if causing issues
}));
3.4 cookie-parser -- Parse Cookies
Makes cookies from the request available on req.cookies.
npm install cookie-parser
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.get('/profile', (req, res) => {
console.log(req.cookies); // { sessionId: 'abc123', theme: 'dark' }
console.log(req.cookies.theme); // 'dark'
res.send('Cookies read!');
});
// Signed cookies (tamper-detection)
app.use(cookieParser('my-secret-key'));
app.get('/signed', (req, res) => {
res.cookie('userId', '42', { signed: true });
res.send('Signed cookie set');
});
app.get('/read-signed', (req, res) => {
console.log(req.signedCookies); // { userId: '42' }
res.send('Signed cookie read');
});
3.5 compression -- Gzip Compression
Compresses response bodies to reduce bandwidth.
npm install compression
const compression = require('compression');
// Compress all responses
app.use(compression());
// With options
app.use(compression({
level: 6, // Compression level (0-9, default 6)
threshold: 1024, // Only compress responses > 1KB
filter: (req, res) => {
// Don't compress if client doesn't support it
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
}
}));
3.6 express-rate-limit -- Rate Limiting
Limits repeated requests from the same IP to prevent abuse.
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// Global rate limit
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Max 100 requests per window per IP
message: {
error: 'Too many requests, please try again later.'
},
standardHeaders: true, // Send rate limit info in headers
legacyHeaders: false, // Disable X-RateLimit-* headers
});
app.use(limiter);
// Stricter limit for auth routes
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Only 5 login attempts per 15 minutes
message: { error: 'Too many login attempts' }
});
app.use('/api/auth', authLimiter);
4. Custom Middleware -- Writing Your Own
When no built-in or third-party option fits, write your own.
// Simple request logger
const requestLogger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
};
app.use(requestLogger);
// API key checker
const requireApiKey = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== process.env.API_KEY) {
return res.status(401).json({ error: 'Invalid or missing API key' });
}
next();
};
app.use('/api', requireApiKey);
// Response time tracker
const responseTimer = (req, res, next) => {
const start = process.hrtime.bigint();
res.on('finish', () => {
const end = process.hrtime.bigint();
const durationMs = Number(end - start) / 1e6;
console.log(`${req.method} ${req.url} -- ${durationMs.toFixed(2)}ms`);
});
next();
};
app.use(responseTimer);
See 3.6.e for advanced custom middleware patterns.
5. Comparison Table
| Feature | Built-in | Third-party | Custom |
|---|---|---|---|
| Installation | None (comes with Express) | npm install <package> | Write it yourself |
| Maintenance | Express team | Package maintainer | You |
| Use case | Body parsing, static files | Security, logging, CORS | App-specific logic |
| Configuration | Options object | Options object | Your design |
| Testing | Tested by Express | Tested by community | You must test |
| Examples | express.json() | helmet(), cors() | Auth guard, logger |
Decision guide:
Need to parse JSON/form bodies? --> express.json() / express.urlencoded()
Need to serve static files? --> express.static()
Need CORS support? --> npm install cors
Need request logging? --> npm install morgan
Need security headers? --> npm install helmet
Need rate limiting? --> npm install express-rate-limit
Need something app-specific? --> Write custom middleware
6. Typical Middleware Stack
Here is a real-world Express setup combining all three types:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const rateLimit = require('express-rate-limit');
const app = express();
// ---------- Security ----------
app.use(helmet()); // Third-party: security headers
// ---------- CORS ----------
app.use(cors({ origin: process.env.CLIENT_URL })); // Third-party: cross-origin
// ---------- Compression ----------
app.use(compression()); // Third-party: gzip
// ---------- Body Parsing ----------
app.use(express.json({ limit: '10kb' })); // Built-in: JSON bodies
app.use(express.urlencoded({ extended: true })); // Built-in: form data
// ---------- Cookies ----------
app.use(cookieParser()); // Third-party: cookies
// ---------- Logging ----------
app.use(morgan('dev')); // Third-party: request logs
// ---------- Rate Limiting ----------
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });
app.use('/api', limiter); // Third-party: abuse prevention
// ---------- Static Files ----------
app.use(express.static('public')); // Built-in: static assets
// ---------- Custom Middleware ----------
app.use((req, res, next) => { // Custom: request ID
req.id = Math.random().toString(36).slice(2, 10);
next();
});
// ---------- Routes ----------
app.get('/', (req, res) => {
res.json({ status: 'ok', requestId: req.id });
});
// ---------- Error Handler ----------
app.use((err, req, res, next) => { // Custom: error handler
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong' });
});
app.listen(3000);
7. Installing and Configuring -- Quick Reference
| Package | Install Command | Basic Usage |
|---|---|---|
cors | npm install cors | app.use(cors()) |
morgan | npm install morgan | app.use(morgan('dev')) |
helmet | npm install helmet | app.use(helmet()) |
cookie-parser | npm install cookie-parser | app.use(cookieParser()) |
compression | npm install compression | app.use(compression()) |
express-rate-limit | npm install express-rate-limit | app.use(rateLimit({ windowMs, max })) |
Install all common middleware at once:
npm install cors morgan helmet cookie-parser compression express-rate-limit
Key Takeaways
- Built-in middleware (
express.json(),express.urlencoded(),express.static()) covers body parsing and static file serving -- no npm install needed. - Third-party middleware handles cross-cutting concerns: security (
helmet), CORS (cors), logging (morgan), and rate limiting (express-rate-limit). - Custom middleware is for app-specific logic that no existing package provides.
- A typical production app uses all three types layered together in a deliberate order.
- Always check if a well-maintained package exists before writing custom middleware for common concerns.
Explain-It Challenge
Explain without notes:
- Name the three built-in Express middleware functions and what each does.
- When would you use
corsmiddleware? What problem does it solve? - What is the difference between
morgan('dev')andmorgan('combined')? - You need to reject requests that exceed 50 requests per minute. Which package do you reach for, and how would you configure it?
Navigation: <- 3.6.a Understanding Middleware | 3.6.c -- Application-Level Middleware ->