Express middleware provides powerful request processing. Here's how to use and create middleware effectively.
Middleware Basics#
1import express, { Request, Response, NextFunction } from 'express';
2
3const app = express();
4
5// Simple middleware
6function logger(req: Request, res: Response, next: NextFunction) {
7 console.log(`${req.method} ${req.url}`);
8 next();
9}
10
11// Apply to all routes
12app.use(logger);
13
14// Apply to specific path
15app.use('/api', logger);
16
17// Apply to specific route
18app.get('/users', logger, (req, res) => {
19 res.json({ users: [] });
20});
21
22// Middleware factory
23function delayMiddleware(ms: number) {
24 return (req: Request, res: Response, next: NextFunction) => {
25 setTimeout(next, ms);
26 };
27}
28
29app.use(delayMiddleware(100));Request Processing Chain#
1// Order matters - middleware runs sequentially
2app.use(express.json()); // 1. Parse JSON body
3app.use(express.urlencoded()); // 2. Parse URL-encoded body
4app.use(cors()); // 3. Handle CORS
5app.use(helmet()); // 4. Security headers
6app.use(morgan('combined')); // 5. Request logging
7app.use(authMiddleware); // 6. Authentication
8app.use('/api', rateLimiter); // 7. Rate limiting
9app.use('/api', routes); // 8. Route handlers
10app.use(errorHandler); // 9. Error handling (last)
11
12// Response modification
13function addHeaders(req: Request, res: Response, next: NextFunction) {
14 res.setHeader('X-Request-Id', generateId());
15 res.setHeader('X-Response-Time', '');
16
17 const start = Date.now();
18
19 res.on('finish', () => {
20 const duration = Date.now() - start;
21 console.log(`Request took ${duration}ms`);
22 });
23
24 next();
25}Authentication Middleware#
1import jwt from 'jsonwebtoken';
2
3interface AuthRequest extends Request {
4 user?: {
5 id: string;
6 email: string;
7 role: string;
8 };
9}
10
11async function authenticate(
12 req: AuthRequest,
13 res: Response,
14 next: NextFunction
15) {
16 const authHeader = req.headers.authorization;
17
18 if (!authHeader?.startsWith('Bearer ')) {
19 return res.status(401).json({ error: 'No token provided' });
20 }
21
22 const token = authHeader.slice(7);
23
24 try {
25 const payload = jwt.verify(token, process.env.JWT_SECRET!) as {
26 userId: string;
27 email: string;
28 role: string;
29 };
30
31 req.user = {
32 id: payload.userId,
33 email: payload.email,
34 role: payload.role,
35 };
36
37 next();
38 } catch (error) {
39 return res.status(401).json({ error: 'Invalid token' });
40 }
41}
42
43// Role-based authorization
44function authorize(...allowedRoles: string[]) {
45 return (req: AuthRequest, res: Response, next: NextFunction) => {
46 if (!req.user) {
47 return res.status(401).json({ error: 'Not authenticated' });
48 }
49
50 if (!allowedRoles.includes(req.user.role)) {
51 return res.status(403).json({ error: 'Forbidden' });
52 }
53
54 next();
55 };
56}
57
58// Usage
59app.get('/admin', authenticate, authorize('admin'), (req, res) => {
60 res.json({ message: 'Admin area' });
61});
62
63app.get('/profile', authenticate, (req: AuthRequest, res) => {
64 res.json({ user: req.user });
65});Validation Middleware#
1import { z } from 'zod';
2
3function validate<T>(schema: z.ZodType<T>) {
4 return (req: Request, res: Response, next: NextFunction) => {
5 try {
6 req.body = schema.parse(req.body);
7 next();
8 } catch (error) {
9 if (error instanceof z.ZodError) {
10 return res.status(400).json({
11 error: 'Validation failed',
12 issues: error.issues.map((issue) => ({
13 path: issue.path.join('.'),
14 message: issue.message,
15 })),
16 });
17 }
18 next(error);
19 }
20 };
21}
22
23const createUserSchema = z.object({
24 email: z.string().email(),
25 password: z.string().min(8),
26 name: z.string().min(2),
27});
28
29app.post('/users', validate(createUserSchema), async (req, res) => {
30 // req.body is validated and typed
31 const user = await createUser(req.body);
32 res.status(201).json(user);
33});
34
35// Query validation
36function validateQuery<T>(schema: z.ZodType<T>) {
37 return (req: Request, res: Response, next: NextFunction) => {
38 try {
39 req.query = schema.parse(req.query) as any;
40 next();
41 } catch (error) {
42 if (error instanceof z.ZodError) {
43 return res.status(400).json({ error: 'Invalid query parameters' });
44 }
45 next(error);
46 }
47 };
48}Error Handling Middleware#
1// Custom error class
2class AppError extends Error {
3 constructor(
4 public statusCode: number,
5 public message: string,
6 public code?: string
7 ) {
8 super(message);
9 this.name = 'AppError';
10 }
11}
12
13// Async error wrapper
14function asyncHandler(
15 fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
16) {
17 return (req: Request, res: Response, next: NextFunction) => {
18 Promise.resolve(fn(req, res, next)).catch(next);
19 };
20}
21
22// Usage
23app.get('/users/:id', asyncHandler(async (req, res) => {
24 const user = await db.users.findUnique({ where: { id: req.params.id } });
25
26 if (!user) {
27 throw new AppError(404, 'User not found', 'USER_NOT_FOUND');
28 }
29
30 res.json(user);
31}));
32
33// Global error handler (must have 4 parameters)
34function errorHandler(
35 error: Error,
36 req: Request,
37 res: Response,
38 next: NextFunction
39) {
40 console.error('Error:', error);
41
42 if (error instanceof AppError) {
43 return res.status(error.statusCode).json({
44 error: error.message,
45 code: error.code,
46 });
47 }
48
49 if (error instanceof z.ZodError) {
50 return res.status(400).json({
51 error: 'Validation error',
52 issues: error.issues,
53 });
54 }
55
56 // Default error response
57 res.status(500).json({
58 error: process.env.NODE_ENV === 'production'
59 ? 'Internal server error'
60 : error.message,
61 });
62}
63
64// Must be last
65app.use(errorHandler);Request Logging#
1import morgan from 'morgan';
2
3// Predefined formats
4app.use(morgan('combined')); // Apache combined format
5app.use(morgan('dev')); // Colored dev format
6
7// Custom format
8morgan.token('body', (req: Request) => JSON.stringify(req.body));
9
10app.use(morgan(':method :url :status :response-time ms - :body'));
11
12// Custom logger middleware
13function requestLogger(req: Request, res: Response, next: NextFunction) {
14 const start = Date.now();
15 const requestId = crypto.randomUUID();
16
17 // Add request ID to request and response
18 req.headers['x-request-id'] = requestId;
19 res.setHeader('x-request-id', requestId);
20
21 res.on('finish', () => {
22 const duration = Date.now() - start;
23
24 console.log(JSON.stringify({
25 requestId,
26 method: req.method,
27 url: req.url,
28 status: res.statusCode,
29 duration,
30 userAgent: req.headers['user-agent'],
31 ip: req.ip,
32 }));
33 });
34
35 next();
36}Caching Middleware#
1import NodeCache from 'node-cache';
2
3const cache = new NodeCache({ stdTTL: 300 });
4
5function cacheResponse(ttlSeconds: number = 300) {
6 return (req: Request, res: Response, next: NextFunction) => {
7 if (req.method !== 'GET') {
8 return next();
9 }
10
11 const key = req.originalUrl;
12 const cached = cache.get(key);
13
14 if (cached) {
15 res.setHeader('X-Cache', 'HIT');
16 return res.json(cached);
17 }
18
19 // Override res.json to cache response
20 const originalJson = res.json.bind(res);
21
22 res.json = (body: any) => {
23 cache.set(key, body, ttlSeconds);
24 res.setHeader('X-Cache', 'MISS');
25 return originalJson(body);
26 };
27
28 next();
29 };
30}
31
32// Usage
33app.get('/products', cacheResponse(600), async (req, res) => {
34 const products = await db.products.findMany();
35 res.json(products);
36});
37
38// Cache invalidation
39function invalidateCache(pattern: string) {
40 const keys = cache.keys();
41 const matchingKeys = keys.filter((key) => key.includes(pattern));
42 cache.del(matchingKeys);
43}
44
45app.post('/products', async (req, res) => {
46 const product = await db.products.create({ data: req.body });
47 invalidateCache('/products');
48 res.status(201).json(product);
49});Composition Patterns#
1// Combine multiple middlewares
2function compose(...middlewares: express.RequestHandler[]) {
3 return (req: Request, res: Response, next: NextFunction) => {
4 const dispatch = (index: number): void => {
5 if (index === middlewares.length) {
6 return next();
7 }
8
9 const middleware = middlewares[index];
10 middleware(req, res, (err) => {
11 if (err) return next(err);
12 dispatch(index + 1);
13 });
14 };
15
16 dispatch(0);
17 };
18}
19
20// Usage
21const apiMiddleware = compose(
22 authenticate,
23 rateLimiter({ max: 100 }),
24 requestLogger
25);
26
27app.use('/api', apiMiddleware);Best Practices#
Organization:
✓ Keep middleware focused
✓ Use middleware factories
✓ Order middleware correctly
✓ Handle async errors
Performance:
✓ Keep middleware lightweight
✓ Cache when appropriate
✓ Avoid blocking operations
✓ Use streaming for large responses
Security:
✓ Validate all inputs
✓ Use helmet for headers
✓ Implement rate limiting
✓ Log security events
Conclusion#
Express middleware enables clean, modular request processing. Use factories for configurable middleware, async handlers for error propagation, and proper ordering for correct execution. Well-designed middleware keeps route handlers focused on business logic.