Role-Based Access Control Patterns for Node.js APIs
Authorization bugs are the most expensive bugs, because they do not crash — they quietly let the wrong person do the right thing. The only reliable place to enforce access control in a Node.js API is the middleware layer, before a request ever reaches business logic. Scatter permission checks through controllers and you guarantee that one route, someday, forgets.
Authentication is not authorization
Knowing who a user is (authentication) is a different problem from knowing what they may do (authorization). JWTs answer the first; they should also carry the claims — roles, permissions — that answer the second. The middleware verifies the token, trusts the signed claims, and decides access from them, without a database round-trip on every request.
// declarative guard at the boundary — controllers stay clean
export const requirePermission =
(perm: string) => (req, res, next) => {
const { permissions } = req.user; // from the verified JWT
if (!permissions.includes(perm)) {
return res.status(403).json({ error: "forbidden" });
}
next();
};
router.delete("/cases/:id", requirePermission("cases:delete"), handler);Permissions, not just roles
Roles are a convenient bundle, but checking role === "admin"everywhere hard-codes policy into your routes. Check fine-grained permissions (cases:delete, reports:export) instead, and let roles be named sets of permissions defined in one place. When the policy changes, you edit the role definition — not a hundred route guards.
Default deny
The safest systems are deny-by-default: a route is inaccessible unless a guard explicitly allows it. A new endpoint shipped without a permission check should fail closed, not fall through to open. That single invariant — nothing is public until someone says so — is what turns access control from a hopeful convention into a guarantee.
An authorization bug never throws. It just lets the wrong request succeed — which is why it has to be impossible by construction, not caught by testing.
For validating what crosses that boundary, see Type-Safe LLMs; RBAC hardens the CMZ enterprise portal.