Express Custom Middleware
In Express.js applications, middleware functions play a crucial role in handling HTTP requests and responses. While Express provides several built-in middleware functions, creating your own custom middleware allows you to add specific functionality tailored to your application's needs.
What is Custom Middleware?
Custom middleware in Express is simply a function that you write yourself to perform operations on the request or response objects, or to execute code during the request-response cycle. These functions have access to:
- The request object (
req
) - The response object (
res
) - The next middleware function in the application's request-response cycle (
next
) - Any errors that might have occurred (
err
)
Basic Structure of Custom Middleware
A custom middleware function follows this basic structure:
function myMiddleware(req, res, next) {
// Do something with req or res
// Call next() to pass control to the next middleware
next();
}
Creating Your First Custom Middleware
Let's start by creating a simple logging middleware that logs the time and method of each request:
const express = require('express');
const app = express();
// Custom logging middleware
const requestLogger = (req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} request to ${req.url}`);
next(); // Don't forget to call next() to continue to the next middleware!
};
// Use the custom middleware
app.use(requestLogger);
// Routes
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
When you make a request to this server, you'll see output like:
[2023-04-15T12:34:56.789Z] GET request to /
The next()
Function
The next()
function is a crucial part of middleware. When called, it passes control to the next middleware function in the stack. If you forget to call next()
, the request will hang and never reach the subsequent middleware or route handlers!
There are three ways to use next()
:
next()
- Pass control to the next middlewarenext('route')
- Skip remaining middleware in this route and go to the next routenext(error)
- Pass an error to Express error handlers
Middleware Application Scopes
You can apply middleware at different levels:
Application-level Middleware
This applies to your entire application:
// Applies to all routes
app.use(requestLogger);
Route-level Middleware
This applies to specific routes:
// Applies only to this route
app.get('/admin', authMiddleware, (req, res) => {
res.send('Admin Dashboard');
});
Router-level Middleware
This applies to a specific router instance:
const router = express.Router();
router.use(requestLogger);
Practical Example: Authentication Middleware
Let's create a more useful middleware that checks if a user is authenticated:
// Authentication middleware
const checkAuth = (req, res, next) => {
// Check for auth token in headers
const authToken = req.headers.authorization;
if (!authToken || !authToken.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Authentication required' });
}
// Extract and verify token (simplified example)
const token = authToken.split(' ')[1];
if (token === 'valid-token') {
// Add user info to request object for use in route handlers
req.user = { id: 123, username: 'exampleuser' };
next();
} else {
res.status(401).json({ message: 'Invalid token' });
}
};
// Protected route
app.get('/profile', checkAuth, (req, res) => {
res.json({
message: 'Profile accessed successfully',
user: req.user
});
});
Error-Handling Middleware
Error-handling middleware has a special signature with four parameters instead of three:
const errorHandler = (err, req, res, next) => {
console.error(`Error: ${err.message}`);
// Send error response
res.status(500).json({
error: {
message: 'An error occurred on the server',
details: process.env.NODE_ENV === 'development' ? err.message : null
}
});
};
// This should be the last middleware added
app.use(errorHandler);
To trigger the error handler from another middleware or route handler:
app.get('/problematic', (req, res, next) => {
try {
// Something that might fail
const result = someFunction();
res.json({ result });
} catch (error) {
next(error); // Pass to error handler
}
});
Real-World Example: Request Processing Pipeline
Let's build a complete middleware pipeline for a typical API endpoint:
const express = require('express');
const app = express();
// Middleware to parse JSON body
app.use(express.json());
// Request logger middleware
const logger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
const start = Date.now();
// When response ends, log the duration
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[${new Date().toISOString()}] Completed ${res.statusCode} in ${duration}ms`);
});
next();
};
// Authentication middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: 'Authentication required' });
}
// Simplified auth check
req.user = { id: 42, role: 'user' };
next();
};
// Rate limiting middleware
const rateLimit = (req, res, next) => {
// Using user ID from authentication middleware
const userId = req.user?.id || 'anonymous';
// Simple in-memory rate limiting (not for production)
const requestCounts = {};
if (!requestCounts[userId]) {
requestCounts[userId] = 1;
} else if (requestCounts[userId] > 100) {
return res.status(429).json({ message: 'Too many requests' });
} else {
requestCounts[userId]++;
}
next();
};
// Apply middleware
app.use(logger);
// Protected routes with multiple middleware
app.get('/api/data', authenticate, rateLimit, (req, res) => {
res.json({
message: 'Data retrieved successfully',
data: { items: [1, 2, 3] },
user: req.user
});
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({ message: 'Server error' });
});
app.listen(3000, () => {
console.log('Server started on port 3000');
});
Creating Configurable Middleware
Make your middleware more flexible by creating a factory function:
// Configurable timeout middleware
const timeout = (limit) => {
return (req, res, next) => {
const timeoutId = setTimeout(() => {
res.status(408).json({ message: 'Request timeout' });
}, limit);
// Clear the timeout when the response is sent
res.on('finish', () => {
clearTimeout(timeoutId);
});
next();
};
};
// Use with different timeouts for different routes
app.get('/fast', timeout(1000), (req, res) => {
res.send('Fast route');
});
app.get('/slow', timeout(5000), (req, res) => {
res.send('Slow route');
});
Summary
Custom middleware is a powerful feature of Express that allows you to:
- Add functionality to the request-response cycle
- Create reusable components for common tasks
- Structure your application's logic in a modular way
- Handle cross-cutting concerns like logging, authentication, and error handling
Remember these key points when creating middleware:
- Always call
next()
(unless you're deliberately ending the response) - Keep middleware functions focused on a single responsibility
- Use middleware application scopes appropriately
- Place error-handling middleware last in your application
Additional Resources
- Express.js Official Middleware Documentation
- Writing Middleware for Express.js
- NPM ecosystem of Express middleware
Exercises
- Create a middleware that tracks the response time for each request and adds it as a header to the response.
- Build an authorization middleware that checks if a user has the correct role to access certain routes.
- Implement a middleware that validates input data based on a schema before it reaches your route handlers.
- Create a caching middleware that stores responses for GET requests and returns the cached response when the same URL is requested again.
- Build a middleware pipeline for a file upload endpoint with authentication, file size validation, and file type checking.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)