Third-Party Middleware — cors, morgan, helmet and express-rate-limit

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
Note: CORS is enforced by the browser — not by Node.js or Express. When your React app (at localhost:5173) calls your Express API (at localhost:5000), the browser checks whether Express sends an 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.
Tip: Use environment variables to control CORS origins. In development allow 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.
Warning: 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);
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 })

🧠 Test Yourself

Your React app at http://localhost:5173 makes a POST request to your Express API. The browser console shows: “Access to fetch at ‘http://localhost:5000/api/posts’ from origin ‘http://localhost:5173’ has been blocked by CORS policy”. What is the most direct fix?