So you scaffolded a blog post and handled permissions in a clean way…
Perhaps…
const isAllowedToUpdate = user.id === author.id || user.role;
const BlogPost = () => {
return (
<BlogPost>
{isAllowedToUpdate && <EditBlogPost />}
</BlogPost>
);
};
Shocker: That was a vulnerable piece of code right there.
It shows how fragile randomly handling permissions with if/else can be.
One little oversight and you're breaking a costly business logic.
So let's fix that.
Let's be more maintainable, reusable, and scalable.
That's the purpose of this article.
Let's get right in.
The Very Basics
I was recently the backend developer for a project that involved 4 roles:
- Pharmacy
- Customer
- Consultant
- Driver
For the sake of clarity, I'll reduce the resources involved to just 3:
- Inventory
- Medical records
- Deliveries
So here's the basics of the relationship.
Customers can:
- Read all inventories
- Read only Medical records assigned to them
- Create orders
- Read only orders they create.
Pharmacies can:
- Create inventories
- Read inventories, but only ones they own
- Update inventories, but only ones they own
- Delete inventories, but only ones they own
Consultants can:
- Create medical records
- Read only medical records they own
- Update only medical records they own
- Delete only medical records they own
Drivers can:
- Read deliveries assigned to them
You may also notice something in addition to all this: consultants have no business with inventory, pharmacies have no business with deliveries, and drivers have no business with medical records — and so on.
The Fast Way
In an Express project, you could quickly put something like this together:
if (user.role === "customer") {
// ...
}
if (user.role === "consultant" && medicalRecord.consultantId === user.id) {
// ...
}
And even better, with middleware.
and have something as clean as this:
authorize(["consultant", "pharmacy"]);
This restricts a particular route from being accessed by any role other than those passed in.
This solves a lot of problems — it protects routes from being accessed by other roles, and quickly narrows down the scope of concern.
The Rise of Spaghetti Code
Think about a situation where the resource in question is an "order" which a customer can only create (not update or delete), a pharmacy can only read if assigned to them, a driver can only read if assigned to them, and a consultant can only read if assigned to them.
My dear brothers and sisters…
You're ending up with:
authorize(["customer", "consultant", "driver", "pharmacy"]);
Whereas you only needed each of these roles to just read "orders" assigned to them.
In a NestJS project, at this point, there's no point calling a roles guard (the Express equivalent).
Just protect the endpoint (controller) generally and move the role logic to the service.
And my dear brothers and sisters, this is how you end up with this...
if (user.role === "consultant") {
if (record.consultantId === user.id) {
// allow
}
} else if (user.role === "pharmacy") {
// ...
} else if (user.role === "driver"
Repeated 20 different places.
This is how you have permission logic bleeding directly into the route handlers.
A spaghetti code.
import { AbilityBuilder, Ability } from '@casl/ability';
import { InventoryPolicy } from './policies/inventory.policy';
import { MedicalPolicy } from './policies/medical.policy';
// ... import other policies
export function createAbilitiesForUser(user) {
const builder = new AbilityBuilder(Ability);
// We pass the user and the builder's methods to each policy
InventoryPolicy(user, builder);
MedicalPolicy(user, builder);
// Add as many as you need...
return builder.build();
}
An ability factory is used in CASL to centralize and dynamically generate a user's permissions based on their identity or role.
It centralizes all authorization rules to live in one single file.
In our case, we had to put our policies inside.
And it eventually finds a way to build a central policy with which our app (across files) works with.
For your Express project, your middleware could then look like this:
const checkPermission = (action, subject) => {
return (req, res, next) => {
const ability = defineAbilitiesFor(req.user);
if (ability.can(action, subject)) {
return next();
}
return res.status(403).json({ message: "Forbidden" });
};
};
You see.
Apt.
And our route becomes as simple as:
router.post('/inventory', checkPermission('create', 'Inventory'), (req, res) => { ... });
Beautiful.
Independent of whatever goes on with our policy, what we change, and what we do.
All it knows to check is "who has the ability" to create inventory based on the created policy.
This allows for flexibility and reusability.
For instance, if we introduce an admin role tomorrow, all we have to do is add it to our policy — and everything still works perfectly.
Now, route protection alone isn't enough.
Let's also reuse this in our service:
// In your Service
async function updateInventory(user, inventoryId, updateData) {
const inventory = await db.inventory.find(inventoryId);
const ability = defineAbilitiesFor(user);
// ABAC logic happens here, away from the if/else mess
if (ability.cannot('update', inventory)) {
throw new ForbiddenException("You do not own this inventory record.");
}
return db.inventory.update(inventoryId, updateData);
}
And voila.
This solves every edge case.
For instance, you may notice in our inventory policy, we had something like this...
if (user.role === 'pharmacy') {
can('manage', 'Inventory', { pharmacyId: user.id });
}
This means pharmacies can manage inventory as long as they own it.
So when we do this...
router.post('/inventory', checkPermission('create', 'Inventory'), (req, res) => { ... });
...we're filtering the forbidden users at the route level.
Immediately.
They never get to the service.
The Upsides
Policies live in one place. Add a role, update the policy file. Done.
Controllers stay dumb. They declare what permission is needed, not how to evaluate it.
Services stay clean. They check ability against the actual object, not the user's role string.
Business logic stays consistent. Because it only exists once.
This is also similarly beautiful for NestJS projects.
If there are requests, I could make a NestJS implementation for this.
I'm a solo developer who's currently a free agent — openly looking for software engineering roles. My portfolio is at www.me.soapnotes.doctor.
Thank you so much!
SOCIAL SHARE CARD GENERATOR