Middleware Is The Framework
If routing is the skeleton of an Express app, middleware is everything else — authentication, logging, validation, error handling, rate limiting. Every production Express app is, at its core, a carefully ordered chain of middleware functions.
Most developers use middleware. Few understand it deeply. That gap shows up in bugs that are hard to trace, security holes that are easy to miss, and code that’s impossible to reuse.
Let’s fix that.
How the Middleware Pipeline Actually Works
(req: Request, res: Response, next: NextFunction) => void
The key is `next`. Calling `next()` passes control to the next middleware in the stack. Not calling it means the request gets stuck — the client waits forever.
Here's the mental model:
Request
↓
[Middleware 1] → next() →
[Middleware 2] → next() →
[Route Handler] → res.json() → Response sent
Order matters absolutely.
Middleware is executed in the order it’s registered. This is a feature — use it intentionally.
import express from 'express';
const app = express();
// 1. Body parsing — must come before routes that read req.body
app.use(express.json());
// 2. Global logging — runs on every request
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next(); // Must call next() or the request hangs
});
// 3. Routes
app.get('/users', getUsers);
// 4. 404 handler — catches any unmatched route
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
Built-in Middleware You Should Know
`express.json()`
app.use(express.json({ limit: '10kb' })); // Limit body size for security
`express.urlencoded()`
app.use(express.urlencoded({ extended: true }));
`express.static()`
app.use('/public', express.static('public'));
// Files in ./public/ are served at /public/file.png
Writing Your Own Reusable Middleware
Request Logger
// src/middleware/logger.ts
import { Request, Response, NextFunction } from 'express';
export function requestLogger(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} ${res.statusCode} — ${duration}ms`);
});
next();
}
Apply it to specific routes — not globally:
router.get('/profile', requireAuth, getUserProfile);
router.put('/profile', requireAuth, updateProfile);
// src/index.ts
import { requestLogger } from './middleware/logger.js';
app.use(requestLogger);
```
### 3b. Authentication Middleware
```typescript
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export function requireAuth(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
(req as any).user = decoded; // Attach user to request
next();
} catch {
res.status(401).json({ error: 'Invalid or expired token' });
}
}
Async Error Handling
This is where most Express apps have bugs.
In Express v4, if an async function throws, Express doesn’t catch it:
// Express v4 — unhandled rejection, server may crash
app.get('/users', async (req, res) => {
const users = await db.getUsers(); // If this throws, Express v4 doesn't catch it
res.json(users);
});
In Express v5, this is fixed — rejected promises are automatically caught.
But you still need a centralized error-handling middleware to return meaningful responses:
// src/middleware/error.ts
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500
) {
super(message);
this.name = 'AppError';
}
}
// 4-argument signature = error middleware (Express convention)
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: err.message
});
}
console.error(err); // Log unexpected errors
res.status(500).json({ error: 'Internal server error' });
}
Register it last in your app:
import { errorHandler } from './middleware/error.js';
// ... all routes ...
app.use(errorHandler); // ← Must be last
```
Now in any route or service, just throw:
```typescript
import { AppError } from '../middleware/error.js';
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) throw new AppError('User not found', 404);
res.json(user);
});
Essential Third-Party Middleware
helmet — Security Headers
// npm install helmet
import helmet from 'helmet';
app.use(helmet()); // Sets 14+ security HTTP headers automatically
cors — Cross-Origin Resource Sharing
// npm install cors @types/cors
import cors from 'cors';
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? '*',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
express-rate-limit — API Rate Limiting
// npm install express-rate-limit
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window per IP
message: { error: 'Too many requests, please slow down.' }
});
app.use('/api', limiter);
morgan — HTTP Request Logging
// npm install morgan @types/morgan
import morgan from 'morgan';
app.use(morgan('combined')); // Apache-style logs
The Correct Middleware Order
// src/index.ts — recommended middleware order
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import morgan from 'morgan';
import rateLimit from 'express-rate-limit';
import { errorHandler } from './middleware/error.js';
import apiRoutes from './routes/index.js';
const app = express();
// 1. Security headers (first, always)
app.use(helmet());
// 2. CORS (before any route that could be preflight-requested)
app.use(cors({ origin: process.env.ALLOWED_ORIGINS }));
// 3. Body parsing
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));
// 4. Request logging
app.use(morgan('dev'));
// 5. Rate limiting
app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
// 6. Routes
app.use('/api', apiRoutes);
// 7. Error handler (must be last)
app.use(errorHandler);
app.listen(3000);
Explore project snapshots or discuss custom web solutions.
Security is always excessive until it's not enough.
Thank You for Spending Your Valuable Time
I truly appreciate you taking the time to read blog. Your valuable time means a lot to me, and I hope you found the content insightful and engaging!
Frequently Asked Questions
The request hangs indefinitely. The client gets a timeout error. Always call `next()` unless you've sent a response.
Yes — pass middleware as an argument between the path and the handler: `app.get('/admin', requireAuth, adminHandler)`.
Express identifies error-handling middleware specifically by function arity (4 args: `err, req, res, next`). With 3 parameters, it won't be treated as an error handler.
Morgan is fine for development. In production, use a structured logger like **Pino** or **Winston** that outputs JSON for log aggregation tools (Datadog, CloudWatch, etc.).
Helmet is a great start — it sets 14 HTTP security headers. But security is layered: also use rate limiting, validate inputs, use HTTPS, and keep dependencies updated.
Comments are closed