The npm ecosystem provides hundreds of middleware packages for Express. Four of them are essential in every production MERN API: cors (cross-origin resource sharing), morgan (HTTP request logging), helmet (security headers), and express-rate-limit (brute-force protection). You have seen cors and helmet referenced in previous chapters — in this lesson you will install all four, understand exactly what each one does, and configure them correctly for both development and production environments.
The Four Essential Third-Party Middleware Packages
| Package | What It Does | Install |
|---|---|---|
| cors | Adds Access-Control-Allow-Origin and related headers so browsers allow cross-origin requests from React to Express |
npm install cors |
| morgan | Logs each HTTP request to the console — method, URL, status, response time | npm install morgan |
| helmet | Sets ~15 security-related HTTP response headers (Content-Security-Policy, X-Frame-Options, etc.) | npm install helmet |
| express-rate-limit | Limits repeated requests from the same IP — blocks brute-force attacks on auth endpoints | npm install express-rate-limit |
Access-Control-Allow-Origin header that permits the React origin. If Express does not send the header, the browser blocks the response. curl, Postman, and server-to-server calls are never affected by CORS — only browser-based JavaScript.http://localhost:5173. In production allow only your actual deployed frontend domain. Never use origin: '*' (allow all origins) on an API that has authenticated routes — it defeats the purpose of CORS as a security boundary.helmet() sets a strict Content-Security-Policy that may block inline scripts, external fonts, and CDN resources from loading in any HTML your Express server renders. If you are serving React’s built index.html through Express in production, you may need to configure helmet’s CSP directives to allow your specific CDN domains and inline script hashes.cors — Cross-Origin Resource Sharing
const cors = require('cors');
// ── Allow all origins (development only — not for production) ─────────────────
app.use(cors());
// ── Allow a single specific origin ────────────────────────────────────────────
app.use(cors({
origin: 'http://localhost:5173',
}));
// ── Allow multiple origins via environment variable ───────────────────────────
const allowedOrigins = [
process.env.CLIENT_URL, // https://yourblog.com
'http://localhost:5173', // local dev
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (curl, Postman, mobile apps)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS: origin ${origin} not allowed`));
}
},
credentials: true, // allow cookies and Authorization headers
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// ── Handle OPTIONS preflight requests ─────────────────────────────────────────
// The browser sends an OPTIONS request before POST/PUT/DELETE with custom headers
// cors() handles this automatically — but you must use app.options('*', cors())
// if you are applying cors() conditionally to specific routes
app.options('*', cors()); // enable preflight for all routes
morgan — HTTP Request Logging
const morgan = require('morgan');
// ── Predefined formats ────────────────────────────────────────────────────────
app.use(morgan('dev'));
// GET /api/posts 200 12.345 ms - 248
// colour-coded by status: green=2xx, yellow=3xx, red=4xx/5xx
app.use(morgan('combined'));
// Apache combined log format — best for production log aggregators
// ::1 - - [01/Jan/2025:10:00:00 +0000] "GET /api/posts HTTP/1.1" 200 248
app.use(morgan('tiny'));
// GET /api/posts 200 248 - 12.345 ms
// ── Only log in development ───────────────────────────────────────────────────
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
// ── Custom format ─────────────────────────────────────────────────────────────
app.use(morgan(':method :url :status :res[content-length] - :response-time ms :date[iso]'));
// ── Morgan format tokens ──────────────────────────────────────────────────────
// :method HTTP method (GET, POST...)
// :url Request URL
// :status HTTP status code
// :response-time Response time in milliseconds
// :date[iso] Request date in ISO 8601
// :remote-addr Client IP address
// :user-agent Client User-Agent string
// :res[header] Any response header value
helmet — Security Headers
const helmet = require('helmet');
// ── Apply all default security headers ────────────────────────────────────────
app.use(helmet());
// Sets these headers automatically:
// Content-Security-Policy
// Cross-Origin-Embedder-Policy
// Cross-Origin-Opener-Policy
// Cross-Origin-Resource-Policy
// X-DNS-Prefetch-Control
// X-Frame-Options: SAMEORIGIN
// Strict-Transport-Security (HSTS)
// X-Content-Type-Options: nosniff
// Origin-Agent-Cluster
// X-Permitted-Cross-Domain-Policies
// Referrer-Policy
// X-XSS-Protection: 0
// ── Configure specific headers ────────────────────────────────────────────────
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // allow inline scripts if needed
styleSrc: ["'self'", 'https://fonts.googleapis.com'],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
imgSrc: ["'self'", 'data:', 'https://res.cloudinary.com'],
},
},
crossOriginResourcePolicy: { policy: 'cross-origin' }, // allow CDN image loading
}));
// ── Disable CSP for APIs that only return JSON (no HTML) ──────────────────────
app.use(helmet({ contentSecurityPolicy: false }));
express-rate-limit — Brute-Force Protection
const rateLimit = require('express-rate-limit');
// ── Global rate limit — applies to all routes ─────────────────────────────────
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minute window
max: 100, // max 100 requests per windowMs per IP
message: { success: false, message: 'Too many requests — please try again in 15 minutes' },
standardHeaders: true, // include RateLimit-* headers in responses
legacyHeaders: false, // disable X-RateLimit-* headers
});
app.use('/api', globalLimiter);
// ── Stricter limit for auth routes (brute-force login prevention) ─────────────
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // only 10 login attempts per 15 minutes per IP
message: { success: false, message: 'Too many login attempts — please try again later' },
skipSuccessfulRequests: true, // don't count successful logins toward the limit
});
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
Recommended Middleware Order in index.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const connectDB = require('./src/config/db');
const app = express();
connectDB();
// 1. Security headers — first, before anything else
app.use(helmet());
// 2. CORS — before routes so preflight OPTIONS are handled
app.use(cors({ origin: process.env.CLIENT_URL || 'http://localhost:5173', credentials: true }));
// 3. Request logging
if (process.env.NODE_ENV === 'development') app.use(morgan('dev'));
// 4. Rate limiting
app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
// 5. Body parsing
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: false }));
// 6. Routes
app.use('/api/posts', require('./src/routes/posts'));
app.use('/api/auth', require('./src/routes/auth'));
// 7. Error handling (always last)
app.use(require('./src/middleware/notFound'));
app.use(require('./src/middleware/errorHandler'));
app.listen(process.env.PORT || 5000);
Common Mistakes
Mistake 1 — Using cors() with wildcard origin on authenticated APIs
❌ Wrong — allowing all origins when routes use authentication:
app.use(cors()); // origin: '*' — allows ANY website to call your authenticated API
✅ Correct — restrict to your known frontend origins:
app.use(cors({ origin: process.env.CLIENT_URL, credentials: true }));
Mistake 2 — Morgan logging in production to stdout
❌ Wrong — using dev-format morgan in production creates noisy, coloured logs that are hard to parse:
app.use(morgan('dev')); // always on — colour codes break log aggregators
✅ Correct — use combined (Apache format) in production for structured log parsing:
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
Mistake 3 — Applying rate limiting after auth routes
❌ Wrong — rate limiter registered after the login route it should protect:
app.use('/api/auth', authRouter); // login route exposed
app.use('/api/auth/login', limiter); // too late — limiter never applied
✅ Correct — rate limiter must be registered before the route:
app.use('/api/auth/login', authLimiter); // applied first ✓
app.use('/api/auth', authRouter);
Quick Reference
| Package | Basic Usage |
|---|---|
| cors | app.use(cors({ origin: process.env.CLIENT_URL })) |
| morgan | app.use(morgan('dev')) |
| helmet | app.use(helmet()) |
| express-rate-limit | app.use('/api', rateLimit({ windowMs: 900000, max: 100 })) |
| Auth rate limit | app.use('/api/auth/login', rateLimit({ max: 10 })) |
| CORS with credentials | cors({ origin: 'url', credentials: true }) |