I wrote about Express middleware chains last year as a pattern for keeping route handlers flat. That was a toy example. This is what it looks like in a real application.
We’re two people building a platform. The home page needs to show the user’s feed, friend recommendations, today’s happenings, friend request count, and unread notifications. That’s five data sources, each requiring its own database query, each needing the user to be authenticated first.
A single route handler doing all of that would be thirty lines of nested callbacks. Instead, we have this:
mw.common = [
mw.cachecheck,
mw.auth,
mw.validate,
notifs.count,
feed.get,
suggestions.get,
happenings.today,
friends.pending
];
app.get('/', mw.common, renderHome);
Eight middleware functions, chained. Each one does exactly one thing, attaches the result to req or res.locals, and calls next(). By the time renderHome runs, everything it needs is already sitting on res.locals:
function renderHome(req, res, next) {
res.render('index');
}
That’s it. All the data loading happened before this function was called. When Jyoti first saw the chain she asked why we had eight functions instead of one. I showed her the alternative – nested callbacks five levels deep. She got the pattern in ten minutes and started writing her own middleware the same week.
How the chain works
cachecheck reads query params. If ?nocache or ?ts is present, it sets no-cache headers. Otherwise it passes through.
authenticate checks the session. If there’s no user, redirect to login with a return URL. If there is, attach the user to res.locals so every template can see it.
validate goes back to the database to refresh the session user. Sessions can go stale – the user might have changed their name or been deactivated since the session was created. This is the only middleware in the chain that hits the database for the user’s sake rather than for data to display.
After that, four data-fetching middleware run in sequence. Each one queries the database and puts results on res.locals:
// notification count
notifs.count = function(req, res, next) {
NotifModel.count(req.session.user, function(err, total) {
res.locals.notifCount = total || 0;
next();
});
};
// friend suggestions
suggestions.get = function(req, res, next) {
UserModel.suggest(req.session.user, function(err, users) {
res.locals.suggestions = users || [];
next(err);
});
};
Same shape, every time. Fetch, attach, next. The template receives notifCount, suggestions, plans, happenings, friendRequests – all populated before the route handler runs.
The fat chain and the thin chain
Not every route needs all eight steps. The home page does – it renders the full dashboard. But API endpoints only need authentication:
app.post('/item/create', mw.auth, item.validate,
item.create, item.respond, item.errors);
app.post('/item/join', mw.auth, item.load,
item.join, item.respond, item.errors);
Two patterns emerged. The fat chain (mw.common) for page renders that need the full context. The thin chain (mw.auth) for API calls that only need to know who’s asking. Route-specific middleware handles the rest – validation, loading, the actual operation, the response.
The item routes also show a different kind of composition. item.validate โ item.create โ item.respond is a pipeline where each step depends on what the previous one attached to req. item.errors sits at the end as the error handler – Express calls it with four arguments (err, req, res, next) when any earlier step calls next(err).
The cost
Every page load runs the full chain. That’s at least five database queries before the route handler does anything. On the home page, feed.get alone fires three Neo4j queries (my items, friend items, friend-joined items), aggregates them, then fires N more for participants and tagged users.
For two people, this is the right tradeoff. The alternative is splitting the data fetching into separate AJAX calls from the client – which means more client-side code, more endpoints, more things to coordinate. We don’t have the people for that. One page load, one chain, all data ready when the template renders. It’s slow but it’s simple, and we can hold the whole thing in our heads.
If the user base grows to where five sequential queries per page load becomes a problem, we’d probably parallelize the queries inside a single step. But that’s a problem for later.
The middleware chains post from last year was about flattening nested callbacks in a single route. We’ve taken the same idea further than I expected – authentication, data loading, rendering, error handling, all composed the same way. For two people building fast, having one pattern everywhere means less code and fewer files to think about.