Building Safer MERN Apps: My Simple Guide to RBAC

So here’s the thing — when I built my first MERN app, everything looked cool on the surface.
Login? ✅
Profile page? ✅
Some admin dashboard? ✅
Until I realised... literally anyone who logged in could open the admin dashboard too. 😬
That’s when I knew — I had to figure out Role-Based Access Control (RBAC).
And trust me, it sounded way scarier than it actually was.
Here’s exactly how I made it work — in the simplest way possible.
First thought: locking some doors
Imagine an office building.
Normal employees can enter their floors.
Managers get access to meeting rooms.
Admins can literally walk anywhere.
In my app, I didn’t have any locks on the doors. Anyone could just wander into the CEO's office.
Bad idea.
So the goal was simple: assign roles and lock doors based on the role.
Step 1: Adding a role to users
When a user signs up, I needed to know who they are — just a normal user? Or an admin?
Here’s what my user schema ended up looking like:
const userSchema = new mongoose.Schema({
name: String,
email: String,
password: String,
role: {
type: String,
enum: ['user', 'manager', 'admin'],
default: 'user'
}
});
Basically, everyone signs up as a user by default.
If I want someone to be an admin later, I update their role manually in the database.
Simple fix.
Step 2: Packing the role inside the JWT
Whenever someone logs in, I send them a JWT token.
Earlier, I only packed the user ID inside the token.
Now, I included their role too:
jwt.sign(
{ id: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
That way, every time they make a request, I know what role they have, without asking the database again and again.
Step 3: Writing the bouncer (middleware)
Next, I needed a bouncer — like a security guy standing at the door, checking if someone’s allowed in.
Here’s the middleware I wrote:
function authorizeRoles(...allowedRoles) {
return (req, res, next) => {
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ message: "Access denied" });
}
next();
};
}
If you’re not on the guest list, you’re not getting in. Period.
Step 4: Guarding the important routes
Now I just had to put my bouncer to work.
Here’s how I protected some routes:
router.get('/admin/dashboard', protect, authorizeRoles('admin'), (req, res) => {
res.send('Welcome, Admin!');
});
router.get('/manager/reports', protect, authorizeRoles('manager', 'admin'), (req, res) => {
res.send('Manager reports page.');
});
So even if a user tries to sneak into /admin/dashboard, the middleware checks their token, sees their role, and kicks them out if they don't belong.
A few mistakes I made (learn from me)
I forgot to verify the token first before checking roles. (Always authenticate first, then authorize.)
I hardcoded roles everywhere instead of using constants.
I made the error messages too detailed at first. (You don’t want to tell attackers why access was denied.)
Some quick tips that helped
Keep roles super simple (
user,manager,admin).Middleware should be small and reusable.
Always log suspicious access attempts (you’ll thank yourself later).
Add a "superadmin" role if you think you’ll need it in the future.
Final thoughts
I won’t lie — at first, RBAC felt like one more complicated thing to learn.
But once I set it up, it felt good knowing my app wasn’t wide open anymore.
If you’re working on a MERN app, even if it’s small — start adding role checks now.
It’s honestly way easier to do early on than to fix later when things are messy.
Trust me. 🙃
Quick summary:
Add a
roleto your user schema.Send the
roleinside the JWT token.Write a small middleware to check allowed roles.
Protect sensitive routes.
And that's it.
RBAC sounds fancy, but it's just common sense, one small step at a time.


