Skip to main content

Command Palette

Search for a command to run...

Building Safer MERN Apps: My Simple Guide to RBAC

Updated
3 min read
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 role to your user schema.

  • Send the role inside 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.