Back to Blog
ExpressNode.jsMiddlewareBackend

Express Middleware Patterns

Master Express middleware for Node.js. From authentication to error handling to custom middleware patterns.

B
Bootspring Team
Engineering
November 20, 2021
6 min read

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.

Share this article

Help spread the word about Bootspring