🧰 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, andexpress.Router - Explain the
extendedoption and when to raise the bodylimit - 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.
📖 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}`);
});
| Option | Library | Can parse nested objects? |
|---|---|---|
extended: false | querystring | No — flat key/value only |
extended: true | qs | Yes — user[name]=Jo → { user: { name: 'Jo' } } |
💡 Analogy: Ifexpress.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 intoreq.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
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.
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 });
});
💡 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:
- Log every request with
morgan('dev'). - Add
helmet()and confirm theX-Powered-Byheader disappears. - Parse JSON so
POST /echocan return the body it received. - Serve a
public/index.htmlat/. - End with a JSON
404handler 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
limitto blunt denial-of-service via huge payloads. - Lock
corsto specific origins in production, not*with credentials. - Use a persistent session store (Redis/DB) outside development.
❌ Don't
- Don't keep
body-parseras a dependency — the built-ins replace it. - Don't ship the default
MemoryStorefor sessions. - Don't place
express.staticafter 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), plusRouter. extended: trueletsurlencodedparse 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.