DEV Community

Young Gao
Young Gao

Posted on

Role-Based Access Control (RBAC) in Node.js: Beyond Simple Admin Checks (2026)

Role-Based Access Control (RBAC) in Node.js: Beyond Simple Admin Checks

Most apps start with if (user.role === "admin"). That works until you need editors who can publish but not delete, moderators who can ban but not edit billing, and viewers who can read but not export.

Define Permissions, Not Roles

const PERMISSIONS = {
  "articles:read": true, "articles:write": true,
  "articles:delete": true, "articles:publish": true,
  "users:read": true, "users:manage": true,
  "billing:read": true, "billing:manage": true,
} as const;

type Permission = keyof typeof PERMISSIONS;
interface Role { name: string; permissions: Permission[]; }
const ROLES: Record<string, Role> = {
  viewer: { name: "Viewer", permissions: ["articles:read", "users:read"] },
  editor: { name: "Editor", permissions: ["articles:read", "articles:write", "articles:publish", "users:read"] },
  admin: { name: "Admin", permissions: ["articles:read", "articles:write", "articles:delete", "articles:publish", "users:read", "users:manage", "billing:read", "billing:manage"] },
};
Enter fullscreen mode Exit fullscreen mode

Authorization Middleware

function requirePermission(...required: Permission[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const userRole = ROLES[req.user.role];
    if (\!userRole) return res.status(403).json({ error: "Unknown role" });
    const hasAll = required.every((p) => userRole.permissions.includes(p));
    if (\!hasAll) return res.status(403).json({ error: "Insufficient permissions" });
    next();
  };
}

// Usage
app.get("/articles", requirePermission("articles:read"), listArticles);
app.post("/articles", requirePermission("articles:write"), createArticle);
app.delete("/articles/:id", requirePermission("articles:delete"), deleteArticle);
app.put("/billing", requirePermission("billing:manage"), updateBilling);
Enter fullscreen mode Exit fullscreen mode

Resource-Level Permissions

Sometimes role alone is not enough. Users should only edit their own articles:

function requireOwnerOrPermission(permission: Permission) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const article = await db.article.findById(req.params.id);
    if (\!article) return res.status(404).json({ error: "Not found" });
    if (article.authorId === req.user.id) return next();
    const userRole = ROLES[req.user.role];
    if (userRole?.permissions.includes(permission)) return next();
    res.status(403).json({ error: "Not authorized" });
  };
}

app.put("/articles/:id", requireOwnerOrPermission("articles:write"), updateArticle);
Enter fullscreen mode Exit fullscreen mode

Checking Permissions in Frontend

Send permissions with the auth token so the UI can conditionally render:

function can(permission: string): boolean {
  return currentUser.permissions.includes(permission);
}

// In JSX: {can("articles:delete") && <DeleteButton />}
Enter fullscreen mode Exit fullscreen mode

When RBAC Is Not Enough

RBAC breaks down when permissions depend on relationships (team membership, org hierarchy, document sharing). For those cases look at:

  • ABAC: policies based on user/resource attributes and environment
  • ReBAC: permissions based on entity relationships (Google Zanzibar model)
  • Libraries: casbin, casl, or managed services like Permit.io and Oso

Part of my Production Backend Patterns series. Follow for more practical backend engineering.


You Might Also Like

Follow me for more production-ready backend content!


If this helped you, buy me a coffee on Ko-fi!

Top comments (0)