A typical Express route handler fetches some data and renders a template:
app.get('/book/:book', function(req, res, next) {
Book.fetch(req.param('book'), function(err, book) {
if (err) return next(err);
Author.fetch(book.author, function(err, author) {
if (err) return next(err);
res.render('book', { book: book, author: author });
});
});
});
Two async calls, three levels of nesting. Add authentication, input validation, permission checks – and you’re five levels deep in a single route definition. The callback alignment pattern helps with formatting, but the nesting is structural. The route handler is doing too many things.
Middleware as steps
Express lets you pass multiple handler functions to a route. Each one gets req, res, and next. Do your work, attach results to req, call next() to pass control to the next handler. Bail out early if something fails.
var fetchBook = function(req, res, next) {
Book.fetch(req.param('book'), function(err, book) {
if (err) return next(err);
req.book = book;
next();
});
};
var fetchBookAuthor = function(req, res, next) {
Author.fetch(req.book.author, function(err, author) {
if (err) return next(err);
req.author = author;
next();
});
};
app.get('/book/:book', fetchBook, fetchBookAuthor, function(req, res) {
res.render('book', { book: req.book, author: req.author });
});
Look at that route definition. fetchBook, then fetchBookAuthor, then render. The intent is visible in the function names. Each step handles its own errors. The final handler is one line.
Why this works
Each middleware function does one thing. It can be tested independently, reused across routes, and understood without reading the rest of the chain. Need fetchBook in another route? Just include it:
app.get('/book/:book/reviews', fetchBook, fetchReviews, function(req, res) {
res.render('reviews', { book: req.book, reviews: req.reviews });
});
The route definition reads top to bottom like a description of what happens. Authenticate, validate, fetch, transform, render. No matter how many steps, the nesting stays flat.
app.get('/user/:id/orders',
authenticate,
validateUserId,
checkPermissions,
fetchUser,
fetchUserOrders,
function(req, res) {
res.json({ orders: req.orders });
}
);
Seven steps, zero nesting. Each function is five to ten lines. The route definition is the documentation.
This pattern changed how we structure every Express application. Once the team saw route handlers as middleware chains instead of monolithic callbacks, the code got simpler across the board. Smaller functions, clearer intent, fewer bugs hiding in nested branches.
Credit to Tomas “wereHamster” Carnecky – the pattern here is from his Nov 2011 post.
