Role-based permissions are more than just toggling features on or off—they’re a foundation for scalable, maintainable products. As a Senior Engineer eyeing a Technical Architect path, you’ll often decide who sees what, ensuring security, consistency, and efficiency across your stack.
In this post, we’ll explore:
The importance of role-based access in UI + backend.
A Strategy Pattern implementation to keep logic clean and flexible.
Best practices to future-proof your approach.
1. Why Roles & Permissions Matter
- Better User Experience: Show only what’s relevant. Users aren’t overwhelmed with irrelevant features.
- Security & Compliance: Minimize accidental or malicious access.
- Cleaner Code: Centralize permission checks; no more copy-paste checks scattered everywhere.
2. Frontend Implementation: Hiding UI Elements
Imagine a React app where certain features (like “Delete User” or “Edit Billing”) are visible only to specific roles. We can use the Strategy Pattern to decide at runtime which set of permissions apply:
// strategy/permissionStrategies.js
// [Strategy Pattern] Each strategy defines a permission set for a specific role.
export const adminPermissions = {
canDeleteUser: true,
canEditBilling: true,
canViewAnalytics: true
};
export const managerPermissions = {
canDeleteUser: false,
canEditBilling: true,
canViewAnalytics: true
};
export const userPermissions = {
canDeleteUser: false,
canEditBilling: false,
canViewAnalytics: true
};
// A helper to get the right strategy object for the current user role
export const getPermissions = (role) => {
switch (role) {
case "admin":
return adminPermissions;
case "manager":
return managerPermissions;
default:
return userPermissions;
}
};
// components/FeatureButton.jsx
import React from "react";
import { getPermissions } from "../strategy/permissionStrategies";
function FeatureButton({ role, feature, onClick, children }) {
const perms = getPermissions(role);
// Only render button if user has the correct permission
if (!perms[feature]) return null;
return (
<button
onClick={onClick}
className="p-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
{children}
</button>
);
}
export default FeatureButton;
How it works
- Strategy Objects (adminPermissions, managerPermissions, userPermissions) define capabilities per role.
- In the component, we lookup permissions based on the user role at runtime.
- If the user cannot perform the feature, we simply don’t render the button.
This approach:
- Keeps UI logic simple—no big if-else blocks in every component.
- Centralizes permission definitions for easy future updates.
3. Backend Implementation: Enforcing Access
Hiding UI elements is good, but server-side checks are crucial. Otherwise, a savvy user could still call APIs directly. Let’s implement a Strategy Pattern in Node.js (Express):
// controllers/permissionStrategyController.js
// [Strategy Pattern] Each strategy defines backend logic for handling roles differently.
const adminStrategy = {
canDeleteUser: () => true,
canEditBilling: () => true,
canViewAnalytics: () => true,
};
const managerStrategy = {
canDeleteUser: () => false,
canEditBilling: () => true,
canViewAnalytics: () => true,
};
const userStrategy = {
canDeleteUser: () => false,
canEditBilling: () => false,
canViewAnalytics: () => true,
};
function getBackendStrategy(role) {
switch (role) {
case "admin":
return adminStrategy;
case "manager":
return managerStrategy;
default:
return userStrategy;
}
}
// Example: Deleting a user in the backend
exports.deleteUser = (req, res) => {
const { userRole } = req.body; // e.g., 'admin', 'manager', 'user'
const strategy = getBackendStrategy(userRole);
if (!strategy.canDeleteUser()) {
return res.status(403).json({ error: "Unauthorized action" });
}
// Proceed with deletion...
// ...
return res.json({ message: "User deleted successfully" });
};
Key Points
- Strategy Pattern: Each role has an object that encapsulates its capabilities.
- Centralized logic: All permissions are in one place, preventing accidental oversights.
- Consistent with Frontend: If the UI hides a feature, the backend also rejects it if requested directly.
4. Why This Makes a Great Product Feature
- User-Centric: Restricting the UI to relevant features improves usability.
- Security: Minimizes potential abuse or accidental misuse by ensuring checks at both the frontend and backend.
- Extensibility: Adding a new role (e.g., supervisor) is as simple as adding another strategy. No rewriting everywhere else.
- Maintainability: Developers quickly find and update permission rules without wading through scattered checks.
5. Areas for Improvement & Best Practices
- Database Storage: Instead of hardcoding, store roles & permissions in a database.
- JWT / Session Management: Ensure user roles in requests are validated (e.g., via tokens).
- Caching: Caching permission sets for frequent lookups can boost performance.
- Logging & Auditing: Keep track of role changes or permission overrides for compliance.
- Test Coverage: Unit tests for each strategy ensure no accidental changes break permissions.
Conclusion
Roles and permissions might seem trivial until your product hits scale or faces security issues. By aligning frontend UI checks with backend server enforcement, using a Strategy Pattern, you keep your code organized, your product safe, and your users happy.
Feel free to adapt this approach to suit your stack—just don’t forget that a well-thought-out permissions system is a massive competitive advantage in any tech product.
Thanks for reading!
If you found this helpful, share your own roles/permissions tips in the comments and let’s learn together.