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

CategorySourceExamples
Built-inShips with Express (no extra install)express.json(), express.urlencoded(), express.static()
Third-partyInstalled via npm installcors, morgan, helmet, cookie-parser, compression, express-rate-limit
CustomWritten by youRequest 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:

ValueParser UsedSupports
trueqs libraryRich objects and arrays (user[name]=Alice)
falsequerystring moduleSimple 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:

HeaderProtection
Content-Security-PolicyPrevents XSS by controlling resource loading
X-Content-Type-Options: nosniffStops browsers from MIME-sniffing
X-Frame-Options: SAMEORIGINPrevents clickjacking
Strict-Transport-SecurityForces HTTPS
X-XSS-Protection: 0Disables buggy browser XSS filter
X-DNS-Prefetch-Control: offControls DNS prefetching
X-Permitted-Cross-Domain-PoliciesRestricts 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

FeatureBuilt-inThird-partyCustom
InstallationNone (comes with Express)npm install <package>Write it yourself
MaintenanceExpress teamPackage maintainerYou
Use caseBody parsing, static filesSecurity, logging, CORSApp-specific logic
ConfigurationOptions objectOptions objectYour design
TestingTested by ExpressTested by communityYou must test
Examplesexpress.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

PackageInstall CommandBasic Usage
corsnpm install corsapp.use(cors())
morgannpm install morganapp.use(morgan('dev'))
helmetnpm install helmetapp.use(helmet())
cookie-parsernpm install cookie-parserapp.use(cookieParser())
compressionnpm install compressionapp.use(compression())
express-rate-limitnpm install express-rate-limitapp.use(rateLimit({ windowMs, max }))

Install all common middleware at once:

npm install cors morgan helmet cookie-parser compression express-rate-limit

Key Takeaways

  1. Built-in middleware (express.json(), express.urlencoded(), express.static()) covers body parsing and static file serving -- no npm install needed.
  2. Third-party middleware handles cross-cutting concerns: security (helmet), CORS (cors), logging (morgan), and rate limiting (express-rate-limit).
  3. Custom middleware is for app-specific logic that no existing package provides.
  4. A typical production app uses all three types layered together in a deliberate order.
  5. Always check if a well-maintained package exists before writing custom middleware for common concerns.

Explain-It Challenge

Explain without notes:

  1. Name the three built-in Express middleware functions and what each does.
  2. When would you use cors middleware? What problem does it solve?
  3. What is the difference between morgan('dev') and morgan('combined')?
  4. 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 ->