Skip to main content

🧰 Built-in and Third-party Middleware

You rarely write everything yourself. Express ships with a handful of built-in middleware for the jobs almost every app needs, and npm offers a rich ecosystem of battle-tested packages for logging, security, sessions, and uploads. This lesson is your field guide to the ones you'll use again and again — and, crucially, the order to stack them in.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Use Express's built-in middleware: express.json, express.urlencoded, express.static, express.raw, express.text, and express.Router
  • Explain the extended option and when to raise the body limit
  • Add key third-party middleware — morgan, cors, helmet, cookie-parser, express-session, compression, multer
  • Assemble a sensible middleware stack in the correct order for a production app
  • Reason about the security and performance trade-offs of each choice

Estimated Time: 40–50 minutes  •  Difficulty: Intermediate

Hands-on: Compose a realistic middleware stack and verify each layer's effect.

In This Lesson

Express Built-in Middleware

Modern Express bundles a small set of middleware for the tasks nearly every server performs: reading request bodies and serving files from disk. They require no extra install — they're methods on the express object itself.

💡 Standard-equipment analogy: Built-in middleware is like the equipment that comes with a new car — headlights, wipers, a steering wheel. They cover the essentials so you don't have to shop for parts before you can drive.
graph TD A[Express App] --> B[express.json] A --> C[express.urlencoded] A --> D[express.static] A --> E[express.raw] A --> F[express.text] A --> G[express.Router] B --> B1[Parses JSON bodies] C --> C1[Parses form data] D --> D1[Serves files from disk] E --> E1[Parses binary bodies] F --> F1[Parses text bodies] G --> G1[Groups modular routes]

📖 A note on history

Older tutorials import the separate body-parser package. Since Express 4.16, the parsers were folded back in — express.json() and express.urlencoded() are the current, first-party way. You no longer need body-parser for JSON or form data.

Body Parsers: json & urlencoded

By default the request body arrives as a raw stream — req.body is undefined until a parser runs. The two parsers you'll use most turn that stream into a usable object.

express.json()

Parses requests whose Content-Type is application/json and populates req.body:

const express = require('express');
const app = express();

app.use(express.json()); // must come BEFORE routes that read req.body

app.post('/api/users', (req, res) => {
  const { name, email } = req.body; // now available
  res.status(201).json({ message: 'User created', user: { name, email } });
});

It accepts an options object; the two you'll actually touch are limit (guard against oversized payloads) and strict (only accept arrays/objects):

app.use(express.json({
  limit: '1mb',   // default is '100kb'
  strict: true    // reject primitives like a bare number or string
}));

express.urlencoded()

Parses classic HTML form submissions (application/x-www-form-urlencoded). The important knob is extended:

app.use(express.urlencoded({ extended: true }));

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  res.send(`Hello ${username}`);
});
OptionLibraryCan parse nested objects?
extended: falsequerystringNo — flat key/value only
extended: trueqsYes — user[name]=Jo{ user: { name: 'Jo' } }
💡 Analogy: If express.json() is a translator fluent in structured JSON documents, express.urlencoded() is a clerk who reads old-fashioned paper forms and files their fields neatly into req.body.

Serving Static Files

express.static() serves files straight from a folder — HTML, CSS, images, client-side JS — without you writing a route for each one.

const path = require('path');

// Files in ./public are served from the URL root:
//   public/styles.css      -> /styles.css
//   public/img/logo.png    -> /img/logo.png
app.use(express.static('public'));

// Mount under a virtual prefix:
//   public/styles.css      -> /assets/styles.css
app.use('/assets', express.static('public'));

// In production, use an absolute path so cwd doesn't matter:
app.use(express.static(path.join(__dirname, 'public')));

A second options argument controls caching and headers — useful for shipping long-lived assets:

app.use(express.static('public', {
  maxAge: '1d',       // Cache-Control max-age
  etag: true,
  lastModified: true,
  setHeaders: (res, filePath) => {
    if (filePath.endsWith('.pdf')) {
      res.set('Content-Disposition', 'attachment');
    }
  }
}));

You can register it more than once to serve from several folders; Express checks them in order and the first match wins:

app.use(express.static('public'));
app.use(express.static('uploads'));

💡 Real-world use

Every image, stylesheet, and script on a site like a typical marketplace is delivered by static-file middleware. Without it, each asset would need its own route handler — enormous, pointless boilerplate.

raw, text & Router

express.raw()

Parses the body into a Buffer — handy for binary payloads or webhook signature verification:

app.use(express.raw({ type: 'application/octet-stream', limit: '10mb' }));

app.post('/upload-binary', (req, res) => {
  console.log('Received bytes:', req.body.length); // req.body is a Buffer
  res.send('Binary received');
});

express.text()

Parses the body into a plain string:

app.use(express.text({ type: 'text/plain', limit: '1mb' }));

app.post('/receive-text', (req, res) => {
  console.log('Got text:', req.body); // req.body is a string
  res.send('Text received');
});

express.Router()

Not a body parser but a built-in class for grouping related routes into a modular, mountable mini-app. It's the backbone of a well-organised codebase:

// routes/api.js
const express = require('express');
const router = express.Router();

router.use((req, res, next) => {   // router-scoped middleware
  console.log('API router hit');
  next();
});

router.get('/', (req, res) => res.send('API home'));
router.get('/users', (req, res) => res.send('Users list'));

module.exports = router;

// app.js
app.use('/api', require('./routes/api')); // -> /api and /api/users
Express built-in middleware building blocks Six cards: express.static serves files; express.json and express.urlencoded parse bodies; express.raw and express.text parse binary and plain text; express.Router groups routes. express.static HTML · CSS · images · JS serves public assets express.json parses JSON payloads application/json express.urlencoded parses form data x-www-form-urlencoded express.raw parses binary bodies → Buffer express.text parses text bodies → string express.Router modular route groups scoped middleware
Figure 1 — The six built-in building blocks: one static server, four body parsers, and the router class.

Essential Third-party Middleware

Beyond the basics, npm supplies well-maintained middleware for the cross-cutting concerns of real apps. Install each with npm install <name>, then app.use() it.

💡 Aftermarket analogy: Third-party middleware is like aftermarket car parts — a better sound system, an alarm, roof racks. Not every driver needs each one, but the right additions transform the experience.
graph LR A[Express App] --> B[morgan] A --> C[cors] A --> D[helmet] A --> E[cookie-parser] A --> F[express-session] A --> G[compression] A --> H[multer] B -->|logging| B1[observability] C -->|cross-origin| C1[access] D -->|HTTP headers| D1[security] E -->|cookies| E1[data] F -->|sessions| F1[auth] G -->|gzip| G1[performance] H -->|uploads| H1[data]

morgan — request logging

const morgan = require('morgan');
app.use(morgan('dev')); // e.g. "GET /home 200 6.1 ms - 1234"

// Write Apache-combined logs to a file:
const fs = require('fs');
const path = require('path');
const stream = fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' });
app.use(morgan('combined', { stream }));

cors — cross-origin requests

By default a browser blocks a page on one origin from calling an API on another. cors sets the headers that permit it — configure the allowed origins tightly in production:

const cors = require('cors');

app.use(cors({
  origin: ['https://example.com', 'https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,   // allow cookies / auth headers
  maxAge: 86400        // cache preflight for 24h
}));
💡 Analogy: CORS is the security desk of a corporate building. By default, visitors from other companies aren't let in; the middleware is the guard checking IDs against an approved list.

helmet — secure HTTP headers

Sets a bundle of sensible security headers (CSP, HSTS, frameguard, noSniff, and more) to defend against common attacks:

const helmet = require('helmet');
app.use(helmet()); // sensible defaults

// Tighten the Content-Security-Policy and HSTS if you need to:
app.use(helmet({
  contentSecurityPolicy: {
    directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", 'trusted-cdn.com'] }
  },
  hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }
}));

cookie-parser — read cookies

const cookieParser = require('cookie-parser');
app.use(cookieParser('my_secret_key')); // secret enables signed cookies

app.get('/set', (req, res) => {
  res.cookie('user', 'john', { maxAge: 900000, httpOnly: true });
  res.cookie('role', 'admin', { signed: true });
  res.send('cookies set');
});

app.get('/read', (req, res) => {
  res.json({ cookies: req.cookies, signed: req.signedCookies });
});

express-session — server-side sessions

const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24 // 24h
  }
}));

app.get('/dashboard', (req, res) => {
  if (!req.session.user) return res.redirect('/login');
  res.send(`Welcome ${req.session.user.username}`);
});

⚠️ Don't ship the default store

The built-in MemoryStore leaks memory and doesn't survive a restart or scale across processes — it's development-only. In production, back sessions with Redis (connect-redis), MongoDB, or Postgres:

const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const client = createClient({ url: process.env.REDIS_URL });
client.connect().catch(console.error);

app.use(session({
  store: new RedisStore({ client }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false
}));

compression & multer

compression gzips responses to cut bandwidth; multer handles multipart/form-data file uploads:

const compression = require('compression');
app.use(compression({ level: 6, threshold: 1024 }));

const multer = require('multer');
const upload = multer({
  dest: 'uploads/',
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
  fileFilter: (req, file, cb) => cb(null, file.mimetype.startsWith('image/'))
});

app.post('/upload', upload.single('avatar'), (req, res) => {
  res.json({ saved: req.file.filename });
});

Composing a Middleware Stack

Order matters. Security and logging go early, parsers before routes, static files before your catch-all, and error handling last. Here's a solid default for a modern app:

const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const app = express();

app.use(morgan('dev'));                              // 1. observe
app.use(helmet());                                   // 2. secure headers
app.use(cors());                                     // 3. cross-origin policy
app.use(express.json());                             // 4. parse JSON
app.use(express.urlencoded({ extended: true }));     // 5. parse forms
app.use(cookieParser());                             // 6. cookies
app.use(compression());                              // 7. compress responses
app.use(express.static('public'));                   // 8. static assets

app.use('/api', require('./routes/api'));            // 9. app routes

app.use((req, res) => res.status(404).json({ error: 'Not found' })); // 10. 404
app.use((err, req, res, next) => {                   // 11. errors — LAST
  console.error(err.stack);
  res.status(err.statusCode || 500).json({ error: err.message });
});
flowchart TB A[HTTP Request] --> B[morgan] B --> C[helmet] C --> D[cors] D --> E[express.json] E --> F[express.urlencoded] F --> G[cookieParser] G --> H[compression] H --> I[express.static] I --> J[App Routes] J --> K[404 Handler] J --> L[Error Handler] K --> M[HTTP Response] L --> M
💡 Analogy: Building a middleware stack is like designing an airport checkpoint. The order of inspections — check ID before or after scanning bags? — changes both security and throughput. Deliberate ordering is the whole game.

Hands-on Exercise

🏋️ Assemble a Production-style Stack

Objective: Build an app that logs, secures, parses, serves static files, and reports 404s — then confirm each layer works.

Requirements:

  1. Log every request with morgan('dev').
  2. Add helmet() and confirm the X-Powered-By header disappears.
  3. Parse JSON so POST /echo can return the body it received.
  4. Serve a public/index.html at /.
  5. End with a JSON 404 handler for unmatched routes.
💡 Hint

Register middleware in the order: morgan → helmet → express.json → express.static → routes → 404. Test the missing X-Powered-By header with curl -I localhost:3000/.

✅ Sample solution
const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const app = express();

app.use(morgan('dev'));
app.use(helmet());              // also hides X-Powered-By
app.use(express.json());
app.use(express.static('public'));

app.post('/echo', (req, res) => {
  res.json({ youSent: req.body });
});

app.use((req, res) => {
  res.status(404).json({ error: `No route for ${req.method} ${req.originalUrl}` });
});

app.listen(3000, () => console.log('http://localhost:3000'));

Check it: curl -X POST -H "Content-Type: application/json" -d '{"hi":1}' localhost:3000/echo echoes the body; curl -I localhost:3000/ shows no X-Powered-By.

Best Practices

✅ Do

  • Register security (helmet) and logging (morgan) early, before routes.
  • Put body parsers before the routes that read req.body.
  • Set a body limit to blunt denial-of-service via huge payloads.
  • Lock cors to specific origins in production, not * with credentials.
  • Use a persistent session store (Redis/DB) outside development.

❌ Don't

  • Don't keep body-parser as a dependency — the built-ins replace it.
  • Don't ship the default MemoryStore for sessions.
  • Don't place express.static after a broad catch-all route that would intercept asset URLs.
  • Don't enable cors() wide-open on an authenticated API.

Summary & Quiz

🎉 Key Takeaways

  • Express's built-ins cover parsing (json, urlencoded, raw, text) and serving files (static), plus Router.
  • extended: true lets urlencoded parse nested form fields.
  • Third-party staples: morgan, cors, helmet, cookie-parser, express-session, compression, multer.
  • The order of the stack drives correctness, security, and performance.
  • Use a real session store and tight CORS in production.

🎯 Quick Quiz

Question 1: Which middleware makes req.body available for a JSON POST request?

Question 2: What does extended: true enable in express.urlencoded()?

Question 3: Why is the default MemoryStore unsuitable for production sessions?

📚 Further Reading

🚀 What's Next?

You've used middleware others wrote. Next you'll write your own — building configurable, reusable custom middleware for logging, authentication, validation, and rate limiting.