Middleware Modules
Now that you understand the basics of modules, let's explore how to create reusable middleware modules for Express. Middleware functions are a powerful way to add functionality that runs before or after your route handlers. Understanding the difference between pre-route and post-route middleware is crucial for building effective Express applications.
Understanding Middleware Execution Order
In Express, middleware functions execute in the order they are defined. The key distinction is:
- Pre-route middleware: Runs BEFORE the route handler. Used for authentication, validation, data parsing, etc.
- Post-route middleware: Runs AFTER the route handler. Used for logging, response modification, cleanup, etc.
The next() function is what controls this flow. When you call next(), Express moves to the next middleware or route handler in the chain.
Pre-Route Middleware: Authentication and Authorization
Pre-route middleware runs before your route handler executes. This is perfect for checking if a user is authenticated, validating input, or performing security checks.
Creating Authentication Middleware
// auth-middleware.js
function requireAuth(req, res, next) {
if (req.session && req.session.userId) {
// User is authenticated, continue to next middleware
next();
} else {
// User is not authenticated, redirect to login
res.redirect('/login');
}
}
function requireAdmin(req, res, next) {
if (req.session && req.session.role === 'admin') {
next();
} else {
// User is not authorized, redirect to access denied page
res.redirect('/access-denied');
}
}
module.exports = {
requireAuth: requireAuth,
requireAdmin: requireAdmin
};
How it works:
requireAuthchecks if the user has a session with auserId- If authenticated, it calls
next()to continue to the route handler - If not authenticated, it redirects to the login page (stops the chain)
Using Pre-Route Middleware
// server.js
const express = require('express');
const exphbs = require('express-handlebars');
const { requireAuth, requireAdmin } = require('./auth-middleware');
const app = express();
// Set up Handlebars as the view engine
app.engine('hbs', exphbs.engine({ extname: '.hbs' }));
app.set('view engine', 'hbs');
app.set('views', './views');
// Protected route - requires authentication
app.get('/profile', requireAuth, (req, res) => {
// This code only runs if requireAuth calls next()
res.render('profile', {
user: req.session.userId,
username: req.session.username
});
});
// Admin route - requires admin role
app.get('/admin', requireAdmin, (req, res) => {
// This code only runs if requireAdmin calls next()
res.render('admin', {
message: 'Admin panel',
user: req.session.userId
});
});
// Login page (no middleware needed)
app.get('/login', (req, res) => {
res.render('login');
});
// Access denied page
app.get('/access-denied', (req, res) => {
res.render('access-denied');
});
Execution flow for /profile:
- Request comes in
requireAuthmiddleware runs first- If authenticated → calls
next()→ route handler runs - If not authenticated → redirects (stops, route handler never runs)
Input Validation Middleware
Here's another example of pre-route middleware for validating user input:
// validation-middleware.js
function validateEmail(req, res, next) {
const email = req.body.email;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email)) {
return res.status(400).json({ error: 'Invalid email address' });
}
next();
}
function validateRequiredFields(fields) {
return (req, res, next) => {
for (const field of fields) {
if (!req.body[field]) {
return res.status(400).json({
error: `Missing required field: ${field}`
});
}
}
next();
};
}
module.exports = {
validateEmail: validateEmail,
validateRequiredFields: validateRequiredFields
};
Use it:
// server.js
const { validateEmail, validateRequiredFields } = require('./validation-middleware');
// Validate email before processing
app.post('/subscribe', validateEmail, (req, res) => {
// Email is valid, process subscription
res.json({ message: 'Subscribed successfully' });
});
// Validate multiple required fields
app.post('/register',
validateRequiredFields(['name', 'email', 'password']),
(req, res) => {
// All required fields are present
res.json({ message: 'Registration successful' });
}
);
Post-Route Middleware: Logging and Response Processing
Post-route middleware runs AFTER your route handler executes. This is useful for logging requests, modifying responses, tracking metrics, or cleaning up resources.
Creating Logging Middleware
Post-route middleware can capture information about the request and response after the route handler has completed:
// logging-middleware.js
function requestLogger(req, res, next) {
// Store the start time
const startTime = Date.now();
// Override res.end to capture response data
const originalEnd = res.end;
res.end = function(chunk, encoding) {
// Calculate response time
const duration = Date.now() - startTime;
// Log the request details
console.log(`${new Date().toISOString()} - ${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`);
// Call the original end method
originalEnd.call(res, chunk, encoding);
};
next();
}
function detailedLogger(req, res, next) {
const startTime = Date.now();
const originalEnd = res.end;
res.end = function(chunk, encoding) {
const duration = Date.now() - startTime;
const logData = {
timestamp: new Date().toISOString(),
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent')
};
console.log(JSON.stringify(logData));
originalEnd.call(res, chunk, encoding);
};
next();
}
module.exports = {
requestLogger: requestLogger,
detailedLogger: detailedLogger
};
Using Post-Route Middleware
// server.js
const express = require('express');
const { requestLogger, detailedLogger } = require('./logging-middleware');
const app = express();
// Apply logging middleware globally (runs for all routes)
app.use(requestLogger);
// Your routes
app.get('/', (req, res) => {
res.send('Home page');
});
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
// The logging middleware will run AFTER each route handler
// and log the request details
Execution flow:
- Request comes in
requestLoggermiddleware runs (sets up response tracking)- Calls
next()→ route handler runs - Route handler sends response
requestLogger's overriddenres.endruns → logs the request
Response Modification Middleware
You can also use post-route middleware to modify responses:
// response-middleware.js
function addTimestamp(req, res, next) {
const originalJson = res.json;
res.json = function(data) {
// Add timestamp to all JSON responses
const dataWithTimestamp = {
...data,
timestamp: new Date().toISOString()
};
return originalJson.call(this, dataWithTimestamp);
};
next();
}
function addRequestId(req, res, next) {
// Generate a unique request ID
req.requestId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const originalJson = res.json;
res.json = function(data) {
const dataWithId = {
...data,
requestId: req.requestId
};
return originalJson.call(this, dataWithId);
};
next();
}
module.exports = {
addTimestamp: addTimestamp,
addRequestId: addRequestId
};
Use it:
// server.js
const { addTimestamp, addRequestId } = require('./response-middleware');
app.use(addRequestId); // Add request ID to all responses
app.get('/api/data', addTimestamp, (req, res) => {
// This response will have both requestId and timestamp
res.json({ message: 'Hello' });
// Response will be: { message: 'Hello', requestId: 'req-...', timestamp: '...' }
});
Combining Pre-Route and Post-Route Middleware
You can combine both types of middleware in a single route:
// server.js
const { requireAuth } = require('./auth-middleware');
const { requestLogger } = require('./logging-middleware');
// Apply logging globally (runs after all routes)
app.use(requestLogger);
// Protected route with authentication
app.get('/dashboard', requireAuth, (req, res) => {
res.render('dashboard', { user: req.session.userId });
});
// Execution order:
// 1. requestLogger sets up tracking
// 2. requireAuth checks authentication
// 3. Route handler renders dashboard
// 4. requestLogger logs the request
Error Handling Middleware
Error handling middleware is a special type of post-route middleware that runs when an error occurs:
// error-handler.js
function handleError(err, req, res, next) {
console.error('Error:', err);
// Default error
let status = 500;
let message = 'Internal server error';
// Handle specific error types
if (err.code === 'SQLITE_CONSTRAINT') {
status = 400;
message = 'Database constraint violation';
} else if (err.message && err.message.includes('UNIQUE constraint')) {
status = 409;
message = 'Resource already exists';
} else if (err.status) {
status = err.status;
message = err.message;
}
res.status(status).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
}
function notFound(req, res, next) {
res.status(404).json({ error: 'Route not found' });
}
module.exports = {
handleError: handleError,
notFound: notFound
};
Use it:
// server.js
const express = require('express');
const { handleError, notFound } = require('./error-handler');
const app = express();
// Your routes here...
app.get('/api/users', (req, res) => {
// If this throws an error, handleError will catch it
const users = getUsersFromDatabase();
res.json(users);
});
// Error handlers must be last
app.use(notFound); // 404 handler (runs if no route matches)
app.use(handleError); // General error handler (runs if error occurs)
Important: Error handling middleware must be defined after all routes and other middleware. It has a special signature: (err, req, res, next) - notice the err parameter first.
Middleware Best Practices
- Use pre-route middleware for: Authentication, authorization, validation, data parsing
- Use post-route middleware for: Logging, response modification, metrics, cleanup
- Always call
next()unless you're intentionally stopping the request (like redirecting) - Error handlers go last - They must be defined after all routes
- Keep middleware focused - One middleware, one responsibility
- Export reusable middleware - Create modules for middleware you'll use in multiple places