Middleware vs Wrapper Functions: Rethinking Authorization in Node.js
Share this article
The Middleware Myth
In many Node.js projects, especially those built with Express or Koa, developers default to a global middleware that intercepts every request to perform authentication checks. The pattern looks familiar:
app.middleware((req, res, next) => {
if (!isProtected(req.path)) return next();
const user = validateRequest(req);
if (user) return next();
res.writeHeader(401);
res.write("Unauthorized");
});
While this approach satisfies the DRY principle at first glance, it introduces a subtle coupling between routing and security logic. The middleware re‑implements routing checks (isProtected) that the framework already provides, and it forces every route to ignore the granular permission model that real applications require.
Author’s Rant
The author argues that “middleware is the wrong abstraction” for authorization. They point out that when roles, scopes, or rate‑limits vary per endpoint, the middleware becomes a tangled web of conditional logic. In the example, the author shows how adding an admin‑only check inside the same middleware forces a loop over a list of patterns, and the logic quickly becomes unreadable.
“Auth is not an independent system from your application. It’s an integral part of it that affects and is affected by everything else.”
This perspective aligns with the principle of separation of concerns: authentication should be a distinct layer that validates the request, while authorization should be applied at the route level.
Wrapper Functions to the Rescue
Instead of a blanket middleware, the author recommends wrapper functions that decorate route handlers. This pattern keeps the authorization logic close to the endpoint that requires it, making the codebase easier to reason about and test.
app.get(
"/",
protectedRoute((req, res, user) => {
// route logic here
})
);
The wrapper receives the authenticated user and can perform fine‑grained checks:
app.get("/moderate", (req, res) => {
if (!hasPermission(req.user.role, ["moderator", "admin"])) {
res.writeHeader(403);
return;
}
// moderation logic
});
This approach eliminates the need for a global check that must remember every protected path. Each handler explicitly declares its requirements, reducing the risk of accidental omission.
When Middleware Still Matters
The article does not dismiss middleware outright. It remains valuable for global concerns such as CSRF protection, request logging, or enriching the request object with user data. The key is to keep authentication as a middleware that attaches a user object, then delegate authorization to route‑specific wrappers.
app.middleware((req, res, next) => {
const user = validateRequest(req);
if (user) req.user = user;
next();
});
With this lightweight middleware, every route can safely access req.user without worrying about authentication failures.
Testing and Debugging
A frequent counter‑argument is that middleware prevents developers from forgetting an auth check. The author counters that rigorous testing of the auth logic—regardless of implementation—supplies the same safety net. In fact, per‑route checks are often easier to debug because the failure point is localized to the handler.
Takeaway
The crux of the argument is that abstraction should serve clarity, not obscure it. Middleware can be a convenient shortcut, but when authorization logic becomes complex, wrapper functions provide a cleaner, more maintainable solution. By keeping authentication and authorization separate—and keeping the latter close to the route that needs it—developers can build safer, more understandable APIs.
Source: https://pilcrowonpaper.com/blog/middleware-auth/