Role-Based Access Control in NestJS with RBAC
Authentication answers 'who is this user?' Authorization answers a different, equally important question: 'is this user allowed to do this specific thing?' Role-Based Access Control (RBAC) is one of the most common authorization models, restricting access to certain routes or actions based on a user's assigned role, such as 'admin', 'editor', or 'customer'.
This lesson builds a complete, reusable RBAC system in NestJS from scratch, combining a custom decorator, metadata, and a guard, extending the authentication foundation from the previous two lessons.
Role Based Access Control NestJS RBAC: Learning Objectives
- Understand the difference between authentication and authorization.
- Attach role requirements to a route using a custom @Roles() decorator and SetMetadata.
- Read that metadata at runtime using NestJS's Reflector class.
- Build a RolesGuard that enforces role requirements against an authenticated user.
- Combine authentication and role-based guards together on the same route.
Role Based Access Control NestJS RBAC: Key Terms and Definitions
- Authorization: The process of determining what an already-authenticated user is permitted to do.
- RBAC (Role-Based Access Control): An authorization model that grants or restricts access based on a user's assigned role.
- SetMetadata: A NestJS function used to attach arbitrary custom metadata to a route handler or controller.
- Reflector: A NestJS utility class used to read metadata previously attached via decorators like SetMetadata.
- Custom decorator: A decorator built using NestJS's own APIs (like SetMetadata) to encapsulate a specific piece of reusable configuration, such as required roles.
How Role Based Access Control NestJS RBAC Works: Detailed Explanation
It's essential to keep authentication and authorization conceptually separate, even though they often work together. Authentication, covered in the previous two lessons, confirms a request comes from a genuine, identifiable user. Authorization takes that already-confirmed identity and decides whether this specific user should be allowed to perform this specific action, such as only allowing users with an 'admin' role to delete other users' accounts.
Building RBAC in NestJS starts with a way to declare which roles a given route requires. Rather than hardcoding this logic inside every guard or controller method, the idiomatic approach creates a custom decorator, conventionally named @Roles(), built using NestJS's SetMetadata function: export const Roles = (...roles: string[]) => SetMetadata('roles', roles). This decorator attaches an array of role names as metadata directly onto whatever route handler it's applied to, without containing any actual authorization logic itself.
The actual enforcement happens in a separate RolesGuard, implementing CanActivate. Inside its canActivate() method, the guard injects NestJS's Reflector class and calls reflector.get<string[]>('roles', context.getHandler()) to read back whatever roles were attached to the currently executing route handler via the @Roles() decorator. If no roles were specified for this route, the guard typically allows the request through unconditionally, since the route wasn't restricted by role in the first place.
If roles were specified, the guard then needs to know the current user's actual roles, which is where the authentication work from the previous lessons pays off directly: assuming an authentication guard (like AuthGuard('jwt')) has already run and populated request.user with the authenticated user's data (including their role or roles), RolesGuard simply compares the required roles against the user's actual roles, allowing the request to proceed only if there's a match, and throwing a ForbiddenException otherwise.
In practice, both guards are applied together on a protected route, in a specific order: @UseGuards(AuthGuard('jwt'), RolesGuard). NestJS executes guards in the order listed, so AuthGuard('jwt') runs first, authenticating the request and populating request.user, and only then does RolesGuard run, since it depends on request.user already being populated to check the user's role against the route's requirements.
Interview-Friendly Explanation
A strong interview or viva answer for role based access control nestjs rbac must go beyond a one-line definition. Interviewers evaluating BCA, MCA, B.Tech, and self-taught backend developers reward answers that combine a precise definition, a clear working explanation, a real-world example, and a conclusion that highlights why it matters for securing production Node.js applications. Use the four-point framework below when answering under time pressure.
- Definition point: Authorization: The process of determining what an already-authenticated user is permitted to do.
- Working point: Attach role requirements to a route using a custom @Roles() decorator and SetMetadata.
- Example point: Admin dashboards and internal tools almost universally implement RBAC exactly this way, restricting destructive or sensitive operations (deleting users, refunding payments) to specific staff roles.
- Conclusion point: RBAC in NestJS is built from three cooperating pieces: a custom decorator, Reflector-based metadata reading, and a guard enforcing the comparison.
How to Answer This in a Technical Interview
- Give a two-to-three sentence definition using correct authentication and security terminology.
- Add one specific example drawn from a real backend scenario such as a banking, SaaS, or e-commerce login flow.
- Mention any security trade-offs (e.g. JWT vs sessions, access vs refresh tokens) since interviewers often probe this contrast.
- Close with one benefit, limitation, or production security consideration.
- Avoid vague answers like "it just checks if the user is logged in" — interviewers filter these out immediately.
Practical Scenario
Imagine you are securing a production SaaS backend, similar to systems used at companies like Razorpay, Freshworks, or Zomato. Every lesson in this module maps directly to a decision you will make while protecting that backend: how users prove who they are, how long that proof stays valid, who is allowed to do what, and how you defend against abuse.
Role Based Access Control NestJS RBAC: Architecture and Flow Diagram
Visualize the RBAC enforcement flow:
[@Roles('admin') attached to a route via SetMetadata] --> [Request arrives] --> [AuthGuard('jwt') authenticates, populates request.user] --> [RolesGuard.canActivate() runs] --> [Reflector reads required roles from route metadata] --> [Compare against request.user.roles] --> [Match? Allow request | No match? Throw ForbiddenException]
Role Based Access Control NestJS RBAC: Concept Reference Table
| Concept | Purpose |
|---|---|
| @Roles(...roles) decorator | Declares which roles a specific route requires, using SetMetadata |
| Reflector | Reads metadata (like required roles) attached to a route handler at runtime |
| RolesGuard | Compares required roles against the authenticated user's actual roles |
| request.user.roles | Populated by an earlier authentication guard, consumed by RolesGuard |
Role Based Access Control NestJS RBAC: NestJS Code Example
// roles.decorator.ts — custom decorator attaching role metadata
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// roles.guard.ts — enforcing the required roles
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>(ROLES_KEY, context.getHandler());
if (!requiredRoles || requiredRoles.length === 0) {
return true; // route has no role restriction
}
const request = context.switchToHttp().getRequest();
const user = request.user; // populated earlier by an authentication guard
const hasRequiredRole = requiredRoles.some((role) => user?.roles?.includes(role));
if (!hasRequiredRole) {
throw new ForbiddenException('You do not have permission to perform this action');
}
return true;
}
}
// users.controller.ts — applying both guards together
import { Controller, Delete, Param, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
@Controller('users')
@UseGuards(AuthGuard('jwt'), RolesGuard) // AuthGuard runs first, then RolesGuard
export class UsersController {
@Delete(':id')
@Roles('admin') // only users with the 'admin' role may call this route
remove(@Param('id') id: string) {
return { message: `User ${id} deleted` };
}
}
The Roles decorator is a thin wrapper around SetMetadata, attaching an array of role names under the key 'roles' to whatever it decorates. RolesGuard injects Reflector and reads this metadata specifically from the current route handler using context.getHandler(); if no roles were required, it allows the request through immediately. Otherwise, it checks whether the authenticated user (assumed to be attached to request.user by an earlier guard) has at least one of the required roles, throwing a ForbiddenException if not. In UsersController, applying @UseGuards(AuthGuard('jwt'), RolesGuard) at the controller level protects every route in this controller with authentication and role-checking, while @Roles('admin') on the specific remove() method declares that only admins may call this particular endpoint.
Real-World Role Based Access Control NestJS RBAC Industry Examples
- Admin dashboards and internal tools almost universally implement RBAC exactly this way, restricting destructive or sensitive operations (deleting users, refunding payments) to specific staff roles.
- SaaS platforms with tiered plans often extend this same pattern beyond simple roles to feature-gating, checking a user's subscription tier metadata instead of (or alongside) a traditional role.
- E-commerce platforms commonly define roles like 'customer', 'vendor', and 'admin', each with access to a different, clearly separated set of API routes protected by exactly this RolesGuard pattern.
- Healthcare and fintech applications, where regulatory compliance often mandates strict access control, frequently extend RBAC further into more granular permission systems, though the foundational @Roles()/RolesGuard pattern remains the starting point.
Role Based Access Control NestJS RBAC Interview Questions and Answers
Q1. What is the difference between authentication and authorization?
Short answer: Authentication confirms who a user is, typically by verifying credentials like a password or a signed JWT. Authorization takes that already-confirmed identity and determines what that specific user is permitted to do, such as restricting certain actions to users with an 'admin' role, which is exactly what RBAC implements.
Detailed explanation: It's essential to keep authentication and authorization conceptually separate, even though they often work together. Authentication, covered in the previous two lessons, confirms a request comes from a genuine, identifiable user. Authorization takes that already-confirmed identity and decides whether this specific user should be allowed to perform this specific action, such as only allowing users with an 'admin' role to delete other users' accounts. Building RBAC in NestJS starts with a way to declare which roles a given route requires. Rather than hardcoding this logic inside every guard or controller method, the idiomatic approach creates a custom decorator, conventionally named @Roles(), built using NestJS's SetMetadata function: export const Roles = (...roles: string[]) => SetMetadata('roles', roles). This decorator attaches an array of role names as metadata directly onto whatever route handler it's applied to, without containing any actual authorization logic itself. The actual enforcement happens in a separate RolesGuard, implementing CanActivate. Inside its canActivate() method, the guard injects NestJS's Reflector class and calls reflector.get<string[]>('roles', context.getHandler()) to read back whatever roles were attached to the currently executing route handler via the @Roles() decorator. If no roles were specified for this route, the guard typically allows the request through unconditionally, since the route wasn't restricted by role in the first place. If roles were specified, the guard then needs to know the current user's actual roles, which is where the authentication work from the previous lessons pays off directly: assuming an authentication guard (like AuthGuard('jwt')) has already run and populated request.user with the authenticated user's data (including their role or roles), RolesGuard simply compares the required roles against the user's actual roles, allowing the request to proceed only if there's a match, and throwing a ForbiddenException otherwise. In practice, both guards are applied together on a protected route, in a specific order: @UseGuards(AuthGuard('jwt'), RolesGuard). NestJS executes guards in the order listed, so AuthGuard('jwt') runs first, authenticating the request and populating request.user, and only then does RolesGuard run, since it depends on request.user already being populated to check the user's role against the route's requirements.
Practical example: Admin dashboards and internal tools almost universally implement RBAC exactly this way, restricting destructive or sensitive operations (deleting users, refunding payments) to specific staff roles.
Interview tip: Authentication = who you are; Authorization (RBAC) = what you're allowed to do.
Revision hook: RBAC in NestJS is built from three cooperating pieces: a custom decorator, Reflector-based metadata reading, and a guard enforcing the comparison.
Q2. How would you implement Role-Based Access Control in NestJS?
Short answer: You create a custom @Roles(...roles) decorator using SetMetadata to attach required role information to specific route handlers, then build a RolesGuard implementing CanActivate that injects Reflector to read this metadata, compares it against the authenticated user's actual roles (populated earlier by an authentication guard), and throws a ForbiddenException if the user lacks the required role.
Detailed explanation: The Roles decorator is a thin wrapper around SetMetadata, attaching an array of role names under the key 'roles' to whatever it decorates. RolesGuard injects Reflector and reads this metadata specifically from the current route handler using context.getHandler(); if no roles were required, it allows the request through immediately. Otherwise, it checks whether the authenticated user (assumed to be attached to request.user by an earlier guard) has at least one of the required roles, throwing a ForbiddenException if not. In UsersController, applying @UseGuards(AuthGuard('jwt'), RolesGuard) at the controller level protects every route in this controller with authentication and role-checking, while @Roles('admin') on the specific remove() method declares that only admins may call this particular endpoint.
Practical example: SaaS platforms with tiered plans often extend this same pattern beyond simple roles to feature-gating, checking a user's subscription tier metadata instead of (or alongside) a traditional role.
Interview tip: @Roles(...roles) decorator uses SetMetadata() to attach required roles to a route handler.
Revision hook: Keeping the @Roles() decorator free of actual logic, and RolesGuard free of hardcoded role names, keeps the whole system reusable across every route.
Q3. Why must an authentication guard run before a role-based guard in the same request?
Short answer: RolesGuard depends on request.user already being populated with the authenticated user's roles, which is the responsibility of an earlier authentication guard like AuthGuard('jwt'). Since NestJS executes guards in the order they're listed in @UseGuards(), the authentication guard must be listed first so RolesGuard has valid user data to check against.
Detailed explanation: Role-Based Access Control (RBAC) in NestJS is built by combining a custom @Roles() decorator, using SetMetadata to attach required role names directly to a route handler, with a RolesGuard that uses NestJS's Reflector class to read that metadata at runtime and compare it against the authenticated user's actual roles. Because RolesGuard depends on request.user already being populated, it must run after an authentication guard like AuthGuard('jwt') in the @UseGuards() chain. This reusable pattern, decorator plus metadata plus guard, cleanly separates the concern of authorization (what a user can do) from authentication (who the user is), forming the standard approach to role-based route protection across real-world NestJS applications.
Practical example: E-commerce platforms commonly define roles like 'customer', 'vendor', and 'admin', each with access to a different, clearly separated set of API routes protected by exactly this RolesGuard pattern.
Interview tip: RolesGuard uses Reflector.get() to read that metadata and compares it against request.user's roles.
Revision hook: Guard execution order is not arbitrary — dependencies between guards (like RolesGuard needing request.user) must be respected.
Q4. What happens if a route has no @Roles() decorator applied, but RolesGuard is still active on its controller?
Short answer: RolesGuard's Reflector.get() call would return undefined (no roles metadata found), and a well-implemented RolesGuard should treat this as 'no restriction' and allow the request to proceed, since the absence of a role requirement means the route wasn't meant to be role-restricted in the first place.
Detailed explanation: It's essential to keep authentication and authorization conceptually separate, even though they often work together. Authentication, covered in the previous two lessons, confirms a request comes from a genuine, identifiable user. Authorization takes that already-confirmed identity and decides whether this specific user should be allowed to perform this specific action, such as only allowing users with an 'admin' role to delete other users' accounts. Building RBAC in NestJS starts with a way to declare which roles a given route requires. Rather than hardcoding this logic inside every guard or controller method, the idiomatic approach creates a custom decorator, conventionally named @Roles(), built using NestJS's SetMetadata function: export const Roles = (...roles: string[]) => SetMetadata('roles', roles). This decorator attaches an array of role names as metadata directly onto whatever route handler it's applied to, without containing any actual authorization logic itself. The actual enforcement happens in a separate RolesGuard, implementing CanActivate. Inside its canActivate() method, the guard injects NestJS's Reflector class and calls reflector.get<string[]>('roles', context.getHandler()) to read back whatever roles were attached to the currently executing route handler via the @Roles() decorator. If no roles were specified for this route, the guard typically allows the request through unconditionally, since the route wasn't restricted by role in the first place. If roles were specified, the guard then needs to know the current user's actual roles, which is where the authentication work from the previous lessons pays off directly: assuming an authentication guard (like AuthGuard('jwt')) has already run and populated request.user with the authenticated user's data (including their role or roles), RolesGuard simply compares the required roles against the user's actual roles, allowing the request to proceed only if there's a match, and throwing a ForbiddenException otherwise. In practice, both guards are applied together on a protected route, in a specific order: @UseGuards(AuthGuard('jwt'), RolesGuard). NestJS executes guards in the order listed, so AuthGuard('jwt') runs first, authenticating the request and populating request.user, and only then does RolesGuard run, since it depends on request.user already being populated to check the user's role against the route's requirements.
Practical example: Healthcare and fintech applications, where regulatory compliance often mandates strict access control, frequently extend RBAC further into more granular permission systems, though the foundational @Roles()/RolesGuard pattern remains the starting point.
Interview tip: Guard order matters: authentication guards must run before role-checking guards that depend on request.user.
Revision hook: This same metadata + Reflector + guard pattern extends naturally to other authorization needs beyond simple roles, like permissions or feature flags.
Role Based Access Control NestJS RBAC MCQs and Practice Questions
1. What does RBAC stand for?
- Remote Basic Access Control
- Role-Based Access Control
- Resource-Based Authorization Check
- Request-Based Access Control
Answer: B. Role-Based Access Control
Explanation: RBAC stands for Role-Based Access Control, an authorization model that grants or restricts access to resources based on a user's assigned role.
Concept link: Authentication = who you are; Authorization (RBAC) = what you're allowed to do.
Why this matters: RBAC in NestJS is built from three cooperating pieces: a custom decorator, Reflector-based metadata reading, and a guard enforcing the comparison.
2. Which NestJS function is used to attach custom metadata (like required roles) to a route handler?
- Reflector.set()
- SetMetadata()
- @Injectable()
- context.getHandler()
Answer: B. SetMetadata()
Explanation: SetMetadata() is the NestJS function used to attach arbitrary key-value metadata to a class or method, forming the basis of custom decorators like @Roles().
Concept link: @Roles(...roles) decorator uses SetMetadata() to attach required roles to a route handler.
Why this matters: Keeping the @Roles() decorator free of actual logic, and RolesGuard free of hardcoded role names, keeps the whole system reusable across every route.
3. Which class is used inside a guard to read metadata previously attached via SetMetadata?
- Injector
- Reflector
- ExecutionContext (alone)
- ModuleRef
Answer: B. Reflector
Explanation: Reflector provides methods like get() to retrieve metadata attached to a route handler or class, which a guard like RolesGuard uses to read required roles set by a custom decorator.
Concept link: RolesGuard uses Reflector.get() to read that metadata and compares it against request.user's roles.
Why this matters: Guard execution order is not arbitrary — dependencies between guards (like RolesGuard needing request.user) must be respected.
4. In @UseGuards(AuthGuard('jwt'), RolesGuard), why must AuthGuard('jwt') be listed first?
- Guards execute in reverse order automatically
- RolesGuard depends on request.user being populated by AuthGuard first
- It has no effect on order
- RolesGuard must always run before authentication
Answer: B. RolesGuard depends on request.user being populated by AuthGuard first
Explanation: NestJS executes guards in the order listed, and since RolesGuard needs to check the authenticated user's roles from request.user, the authentication guard that populates this data must run first.
Concept link: Guard order matters: authentication guards must run before role-checking guards that depend on request.user.
Why this matters: This same metadata + Reflector + guard pattern extends naturally to other authorization needs beyond simple roles, like permissions or feature flags.
Common Role Based Access Control NestJS RBAC Mistakes to Avoid
- Confusing authentication and authorization, using a single guard to try to handle both identity verification and role checking simultaneously.
- Listing RolesGuard before the authentication guard in @UseGuards(), causing RolesGuard to run against an empty or undefined request.user.
- Hardcoding required roles directly inside a guard's logic instead of using a reusable @Roles() decorator with Reflector, making the guard inflexible.
- Forgetting to handle the case where a route has no @Roles() metadata at all, accidentally blocking unrestricted routes instead of allowing them through.
Role Based Access Control NestJS RBAC: Interview Notes and Exam Tips
- Authentication = who you are; Authorization (RBAC) = what you're allowed to do.
- @Roles(...roles) decorator uses SetMetadata() to attach required roles to a route handler.
- RolesGuard uses Reflector.get() to read that metadata and compares it against request.user's roles.
- Guard order matters: authentication guards must run before role-checking guards that depend on request.user.
Key Role Based Access Control NestJS RBAC Takeaways
- RBAC in NestJS is built from three cooperating pieces: a custom decorator, Reflector-based metadata reading, and a guard enforcing the comparison.
- Keeping the @Roles() decorator free of actual logic, and RolesGuard free of hardcoded role names, keeps the whole system reusable across every route.
- Guard execution order is not arbitrary — dependencies between guards (like RolesGuard needing request.user) must be respected.
- This same metadata + Reflector + guard pattern extends naturally to other authorization needs beyond simple roles, like permissions or feature flags.
Role Based Access Control NestJS RBAC: Summary
Role-Based Access Control (RBAC) in NestJS is built by combining a custom @Roles() decorator, using SetMetadata to attach required role names directly to a route handler, with a RolesGuard that uses NestJS's Reflector class to read that metadata at runtime and compare it against the authenticated user's actual roles. Because RolesGuard depends on request.user already being populated, it must run after an authentication guard like AuthGuard('jwt') in the @UseGuards() chain. This reusable pattern, decorator plus metadata plus guard, cleanly separates the concern of authorization (what a user can do) from authentication (who the user is), forming the standard approach to role-based route protection across real-world NestJS applications.