Skip to Content
chalvien 1.0 is released

Role-Based Access Control (RBAC) in Node.js and React

RBAC answers one core question in every application: who is allowed to do what?

As soon as your app has multiple user types, such as admins, editors, and viewers, access control should become explicit. Without it, authorization rules spread across controllers and components, become hard to audit, and eventually create security bugs.

Role-Based Access Control (RBAC) solves this by assigning users to roles and mapping each role to allowed actions. It gives you a clear policy that is easier to reason about, test, and maintain.

Use RBAC when:

  • Your user groups are stable (for example, admin/editor/viewer).
  • Most permissions are shared by groups rather than customized per user.
  • You want predictable authorization checks on both API and UI layers.

In this guide, you will implement RBAC in three practical layers:

  1. Define roles and permissions.
  2. Enforce permissions in Node.js backend routes.
  3. Reflect permissions in React route guards and UI.

1. Define Roles and Permissions

Start by centralizing your authorization model. Keep it in one place so backend and frontend can follow the same policy.

// permissions.js const roles = { admin: { can: ['create', 'edit', 'delete', 'view'], }, editor: { can: ['create', 'edit', 'view'], }, viewer: { can: ['view'], }, }; module.exports = { roles };

Role summary:

  • Admin: full access.
  • Editor: create, edit, and view.
  • Viewer: view only.

2. Attach the User Role During Authentication

After authentication (for example, JWT verification), attach the role to req.user so route middleware can evaluate permissions consistently.

// authMiddleware.js (example shape) // req.user should be available before authorization middleware runs. req.user = { id: decoded.sub, role: decoded.role, };

3. Enforce RBAC in Node.js Routes

Authorization must be enforced on the backend. Frontend checks improve UX, but they do not provide security.

// roleMiddleware.js const { roles } = require('./permissions'); const checkPermission = (action) => { return (req, res, next) => { const userRole = req.user?.role; if (!userRole || !roles[userRole]) { return res.status(401).json({ message: 'Unauthorized' }); } const permissions = roles[userRole].can; if (!permissions.includes(action)) { return res.status(403).json({ message: 'Forbidden' }); } next(); }; }; module.exports = { checkPermission };

Use the middleware at route level:

// app.js const express = require('express'); const { checkPermission } = require('./roleMiddleware'); const app = express(); app.post('/content', checkPermission('create'), (req, res) => { res.send('Content created'); }); app.delete('/content/:id', checkPermission('delete'), (req, res) => { res.send('Content deleted'); });

4. Handle Role-Based Routing in React

In React, use role-aware route guards to guide users away from pages they should not access.

// auth.js import { jwtDecode } from 'jwt-decode'; export const getUserRole = () => { const token = localStorage.getItem('token'); if (!token) return null; try { const decoded = jwtDecode(token); return decoded.role ?? null; } catch { return null; } };
// ProtectedRoute.jsx (React Router v6) import { Navigate, Outlet } from 'react-router-dom'; import { getUserRole } from './auth'; const ProtectedRoute = ({ allowedRoles }) => { const role = getUserRole(); if (!role) return <Navigate to="/login" replace />; if (!allowedRoles.includes(role)) return <Navigate to="/unauthorized" replace />; return <Outlet />; }; export default ProtectedRoute;
// App.jsx import { BrowserRouter, Route, Routes } from 'react-router-dom'; import ProtectedRoute from './ProtectedRoute'; import AdminDashboard from './AdminDashboard'; import EditorPage from './EditorPage'; import ViewerPage from './ViewerPage'; import Unauthorized from './Unauthorized'; const App = () => ( <BrowserRouter> <Routes> <Route element={<ProtectedRoute allowedRoles={['admin']} />}> <Route path="/admin" element={<AdminDashboard />} /> </Route> <Route element={<ProtectedRoute allowedRoles={['admin', 'editor']} />}> <Route path="/editor" element={<EditorPage />} /> </Route> <Route element={<ProtectedRoute allowedRoles={['admin', 'editor', 'viewer']} />} > <Route path="/viewer" element={<ViewerPage />} /> </Route> <Route path="/unauthorized" element={<Unauthorized />} /> </Routes> </BrowserRouter> ); export default App;

5. Show or Hide UI by Permission

Use role checks to improve UX, such as hiding actions users cannot perform.

// DeleteButton.jsx import { getUserRole } from './auth'; const DeleteButton = ({ onDelete }) => { const role = getUserRole(); return role === 'admin' ? <button onClick={onDelete}>Delete User</button> : null; }; export default DeleteButton;

This is a convenience for users, not a security boundary. The API must still enforce authorization.

6. Practical End-to-End Example

If your admin dashboard includes user deletion:

  • Frontend: show the delete button only for admin users.
  • Backend: protect DELETE /users/:id with checkPermission('delete').
app.delete('/users/:id', checkPermission('delete'), (req, res) => { res.send('User deleted'); });

Common RBAC Mistakes to Avoid

  • Enforcing authorization only in React and not on backend APIs.
  • Duplicating permission logic across files instead of using one policy map.
  • Returning generic success responses when authorization fails.
  • Hard-coding role strings in many places without shared constants.

Conclusion

RBAC helps you keep authorization predictable and auditable across your stack.

For a reliable implementation:

  1. Define roles and permissions once.
  2. Enforce checks in backend middleware on every protected route.
  3. Mirror those rules in React for better navigation and UX.

With this structure, your app remains secure as features and teams grow.