Skip to main content

Express Middleware Patterns

Middleware functions are the backbone of Express.js applications, creating a pipeline that processes requests before they reach their final route handler. Understanding advanced middleware patterns can dramatically improve your Express application's structure, maintainability, and flexibility.

What is Middleware?

Before diving into advanced patterns, let's refresh our understanding of middleware in Express:

javascript
app.use((req, res, next) => {
// Do something with request or response
console.log('Middleware executed');
next(); // Pass control to the next middleware function
});

A middleware function has access to the request object (req), the response object (res), and the next middleware function (next) in the application's request-response cycle.

Common Middleware Patterns

1. Conditional Middleware

Sometimes, you want middleware to execute only under certain conditions.

javascript
// Execute middleware only when a specific query parameter exists
const conditionalLogger = (req, res, next) => {
if (req.query.debug === 'true') {
console.log('DEBUG:', req.method, req.url);
}
next();
};

app.use(conditionalLogger);

This middleware will only log requests when the URL includes ?debug=true.

2. Middleware Chaining

You can chain multiple middleware functions together for a specific route:

javascript
// Authentication middleware
const authenticate = (req, res, next) => {
const isAuthenticated = /* check authentication */true;
if (isAuthenticated) {
next(); // Continue to next middleware
} else {
res.status(401).send('Unauthorized');
}
};

// Authorization middleware
const authorize = (req, res, next) => {
const isAuthorized = /* check authorization */true;
if (isAuthorized) {
next();
} else {
res.status(403).send('Forbidden');
}
};

// Apply both middleware functions to a route
app.get('/protected', authenticate, authorize, (req, res) => {
res.send('Welcome to protected content!');
});

3. Parameterized Middleware

Creating configurable middleware that behaves differently based on parameters:

javascript
// Configurable rate limiter middleware
const rateLimit = (maxRequests, timeWindow) => {
const requestCounts = {};

return (req, res, next) => {
const ip = req.ip;
const now = Date.now();

// Initialize or clean up old records
requestCounts[ip] = requestCounts[ip]?.filter(time => now - time < timeWindow) || [];

// Check if limit is exceeded
if (requestCounts[ip].length >= maxRequests) {
return res.status(429).send('Too Many Requests');
}

// Record this request
requestCounts[ip].push(now);
next();
};
};

// Use with different configurations for different routes
app.use('/api', rateLimit(100, 60000)); // 100 requests per minute for API
app.use('/login', rateLimit(5, 60000)); // 5 login attempts per minute

4. Error-Handling Middleware

Express has special middleware for handling errors, which takes four parameters:

javascript
// Regular middleware - Won't catch errors
app.use((req, res, next) => {
console.log('Regular middleware');
next();
});

// Error-handling middleware - Note the four parameters
app.use((err, req, res, next) => {
console.error('Error occurred:', err);
res.status(500).send('Something broke!');
});

// Route that might throw an error
app.get('/might-error', (req, res, next) => {
try {
// Something that might fail
if (Math.random() > 0.5) {
throw new Error('Random failure!');
}
res.send('Everything went well!');
} catch (err) {
// Pass error to the error-handling middleware
next(err);
}
});

5. Middleware for Specific Routes

You can apply middleware to specific routes or route groups:

javascript
// Middleware for all routes
app.use(express.json());

// Middleware for a specific route
app.get('/user/:id', validateUserId, (req, res) => {
res.send(`User ID: ${req.params.id}`);
});

// Middleware for a group of routes using router
const apiRouter = express.Router();
apiRouter.use(apiKeyValidator);

apiRouter.get('/data', (req, res) => {
res.json({ data: 'secret stuff' });
});

apiRouter.post('/data', (req, res) => {
res.json({ status: 'data saved' });
});

app.use('/api', apiRouter);

Advanced Middleware Patterns

1. Middleware Composition

Breaking down complex middleware into smaller, reusable functions:

javascript
// A collection of small, focused middleware functions
const validateContentType = (req, res, next) => {
if (req.headers['content-type'] !== 'application/json') {
return res.status(400).send('Content-Type must be application/json');
}
next();
};

const validateJsonSchema = (req, res, next) => {
// Simplified validation
if (!req.body.name || !req.body.email) {
return res.status(400).send('Name and email are required');
}
next();
};

const sanitizeInput = (req, res, next) => {
if (req.body.name) req.body.name = req.body.name.trim();
if (req.body.email) req.body.email = req.body.email.trim().toLowerCase();
next();
};

// Compose them for a specific route
app.post('/user',
validateContentType,
express.json(),
validateJsonSchema,
sanitizeInput,
(req, res) => {
// Handler using validated and sanitized data
res.status(201).send('User created');
}
);

2. Dynamic Middleware

Creating middleware that adapts based on runtime conditions:

javascript
const featureFlags = {
newFeature: process.env.ENABLE_NEW_FEATURE === 'true'
};

// Middleware that conditionally enables features
app.use((req, res, next) => {
// Attach feature flags to request object
req.features = featureFlags;
next();
});

app.get('/feature', (req, res) => {
if (req.features.newFeature) {
res.send('New feature is enabled!');
} else {
res.send('Using legacy version');
}
});

3. Async Middleware Pattern

Handling asynchronous operations properly in middleware:

javascript
// Bad pattern - errors in the promise will be unhandled
app.use((req, res, next) => {
fetchSomeData().then(data => {
req.data = data;
next();
});
// If fetchSomeData rejects, Express won't catch the error
});

// Good pattern - using async/await with proper error handling
app.use(async (req, res, next) => {
try {
req.data = await fetchSomeData();
next();
} catch (error) {
next(error); // Pass errors to Express error handler
}
});

4. Request Processing Pipeline

Creating a multi-stage request processing pipeline with middleware:

javascript
// Example: API request pipeline
const apiPipeline = [
// 1. Parse incoming request
express.json(),

// 2. Authentication
(req, res, next) => {
const token = req.headers.authorization;
if (!token) return res.status(401).send('No token provided');
try {
req.user = verifyToken(token);
next();
} catch (err) {
res.status(401).send('Invalid token');
}
},

// 3. Request validation
(req, res, next) => {
const errors = validateRequest(req.body);
if (errors) return res.status(400).json({ errors });
next();
},

// 4. Business logic middleware
async (req, res, next) => {
try {
req.processedData = await processBusinessLogic(req.body, req.user);
next();
} catch (err) {
next(err);
}
},

// 5. Response formatter
(req, res) => {
res.json({
success: true,
data: req.processedData,
user: req.user.id
});
}
];

app.post('/api/process', apiPipeline);

5. Middleware Factory Pattern

Creating functions that return specialized middleware:

javascript
// A middleware factory for role-based access control
const requireRole = (role) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).send('Not authenticated');
}

if (req.user.role !== role) {
return res.status(403).send(`Requires ${role} role`);
}

next();
};
};

// Usage
app.get('/admin/dashboard', requireRole('admin'), (req, res) => {
res.send('Admin Dashboard');
});

app.get('/user/profile', requireRole('user'), (req, res) => {
res.send('User Profile');
});

Real-World Application Example

Let's build a simple API with authentication, validation, and error handling using the middleware patterns we've learned:

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

// Middleware factory for response time tracking
const responseTime = (options = {}) => {
const threshold = options.threshold || 0;

return (req, res, next) => {
const start = Date.now();

// Hook into response finish event
res.on('finish', () => {
const duration = Date.now() - start;
if (duration > threshold) {
console.log(`${req.method} ${req.url} - ${duration}ms`);
}
});

next();
};
};

// JSON parsing middleware
app.use(express.json());

// Track response times over 100ms
app.use(responseTime({ threshold: 100 }));

// Authentication middleware
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).send('Authentication required');
}

const token = authHeader.split(' ')[1];

try {
// Simplified token verification (in real apps, use JWT or other proper auth)
if (token === 'valid-token') {
req.user = { id: 123, name: 'Example User' };
next();
} else {
res.status(401).send('Invalid token');
}
} catch (err) {
next(err);
}
};

// Validation middleware factory
const validate = (schema) => {
return (req, res, next) => {
// Simplified validation
const errors = [];

for (const [field, required] of Object.entries(schema)) {
if (required && !req.body[field]) {
errors.push(`${field} is required`);
}
}

if (errors.length) {
return res.status(400).json({ errors });
}

next();
};
};

// Routes with middleware chains
app.post('/api/posts',
authenticate,
validate({ title: true, content: true }),
(req, res) => {
// Create post with validated data
const post = {
id: Math.floor(Math.random() * 1000),
title: req.body.title,
content: req.body.content,
author: req.user.id
};

res.status(201).json({ post });
}
);

// Error handling middleware
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({
error: 'Server error',
message: process.env.NODE_ENV === 'production' ? null : err.message
});
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

This example demonstrates several patterns:

  1. Middleware factory pattern with responseTime and validate
  2. Authentication middleware
  3. Request validation
  4. Error handling
  5. Middleware chaining

Summary

Express middleware patterns provide powerful ways to structure your application and handle complex request flows. We've covered:

  • Conditional middleware execution
  • Middleware chaining and composition
  • Parameterized middleware and middleware factories
  • Error handling patterns
  • Route-specific middleware
  • Async middleware handling
  • Request processing pipelines

By mastering these patterns, you can create more maintainable, modular, and robust Express applications.

Additional Resources and Exercises

Resources

Exercises

  1. Basic Middleware Factory: Create a middleware factory that logs requests with configurable log levels (e.g., "debug", "info", "error").

  2. Authentication System: Implement a complete authentication system using middleware patterns with:

    • User registration
    • Login/logout functionality
    • JWT token verification
    • Role-based access control
  3. Caching Middleware: Create a middleware that caches responses for GET requests for a configurable amount of time to improve performance.

  4. Request Validation Pipeline: Build a comprehensive validation pipeline for an API endpoint that processes user data, with separate middleware for:

    • Schema validation
    • Data sanitization
    • Business rule validation
    • Error formatting
  5. Rate Limiting: Implement a more sophisticated rate limiting middleware that uses different limits for authenticated and unauthenticated users.

By practicing these exercises, you'll deepen your understanding of Express middleware patterns and how to apply them effectively in real-world applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)