Suggest an editImprove this articleRefine the answer for “How to implement Role-Based Access Control (RBAC) in NestJS?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**RBAC in NestJS** uses a custom `@Roles()` decorator to stamp required roles on a route handler, and a `RolesGuard` that reads those roles via `Reflector` to allow or block the request. ```typescript @Controller('admin') @UseGuards(AuthGuard('jwt'), RolesGuard) export class AdminController { @Get('dashboard') @Roles(Role.Admin) getDashboard() { return { message: 'Admin dashboard' }; } } // Admin → 200 OK | Regular user → 403 Forbidden ``` **Key point:** always chain `AuthGuard('jwt')` before `RolesGuard`. The JWT guard sets `request.user`; without it the roles guard has nothing to check.Shown above the full answer for quick recall.Answer (EN)Image**RBAC (Role-Based Access Control) in NestJS** is a pattern where a Guard reads role metadata from the handler or controller, compares it to `user.roles` from the request, and passes or blocks the call before your business logic runs. ## Theory ### TL;DR - Analogy: `@Roles()` stamps the door requirement; `RolesGuard` checks the wristband on every request - Core mechanic: `SetMetadata` writes required roles onto the route; `Reflector` reads them inside the Guard - Always put `AuthGuard('jwt')` before `RolesGuard` - the JWT guard populates `request.user`; without it `RolesGuard` crashes - Store roles as an array in JWT (`roles: ['admin', 'moderator']`), not a single string - a single string breaks multi-role users - Under 100 roles with no time or location rules: RBAC is enough. More complex policies: look at Casbin ### Quick example ```typescript // roles.decorator.ts import { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); // roles.guard.ts @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ context.getHandler(), // handler metadata first context.getClass(), // class metadata as fallback ]); if (!requiredRoles) return true; // no decorator = public route const { user } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user?.roles?.includes(role)); } } // admin.controller.ts @Controller('admin') @UseGuards(AuthGuard('jwt'), RolesGuard) // order matters export class AdminController { @Get('dashboard') @Roles(Role.Admin, Role.SuperAdmin) getDashboard() { return { message: 'Admin dashboard' }; } } // Admin request → 200 OK // Regular user → 403 Forbidden ``` `getAllAndOverride` checks the handler first, then falls back to the class. So a `@Roles(Role.Admin)` on the controller class applies to every route inside it unless you override per handler. ### How it works internally NestJS loads `reflect-metadata` when `NestFactory.create()` runs. The `@Roles()` decorator calls `SetMetadata(ROLES_KEY, roles)`, which writes the role array as metadata on the handler or class. When a request arrives, `RolesGuard.canActivate()` runs in the Express middleware chain after `AuthGuard` has already parsed the JWT and attached `user` to `request`. The Reflector reads the metadata, the Guard compares it to `user.roles`, and throws `ForbiddenException` on mismatch. All of this happens before your handler function is ever called. ### Global guard setup You can register `RolesGuard` globally so it applies everywhere without decorating individual controllers: ```typescript // app.module.ts @Module({ providers: [ { provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: RolesGuard }, ], }) export class AppModule {} ``` When you go global, public routes get a 403 unless you handle the no-metadata case. The `if (!requiredRoles) return true` line in the Guard above is what makes undecorated routes pass through. Or add a `@Public()` decorator that the Guard recognizes as a skip signal. ### Permission-based access Roles work for coarse-grained control. If you need "admins can write posts but not delete users", map roles to granular permissions: ```typescript export enum Permission { ReadUsers = 'read:users', WriteUsers = 'write:users', DeleteUsers = 'delete:users', ManageSettings = 'manage:settings', } const rolePermissions: Record<Role, Permission[]> = { [Role.User]: [Permission.ReadUsers], [Role.Moderator]: [Permission.ReadUsers, Permission.WriteUsers], [Role.Admin]: [Permission.ReadUsers, Permission.WriteUsers, Permission.ManageSettings], [Role.SuperAdmin]: Object.values(Permission), // all permissions }; export const RequirePermissions = (...permissions: Permission[]) => SetMetadata('permissions', permissions); @Injectable() export class PermissionsGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const required = this.reflector.getAllAndOverride<Permission[]>( 'permissions', [context.getHandler(), context.getClass()], ); if (!required) return true; const { user } = context.switchToHttp().getRequest(); const userPermissions = user.roles .flatMap((role: Role) => rolePermissions[role] ?? []); return required.every((perm) => userPermissions.includes(perm)); } } ``` This pattern fits SaaS dashboards where roles overlap but permission sets differ. ### Resource ownership Sometimes role alone is not enough. A user should only edit their own posts, but an admin can edit anyone's. That needs an ownership check separate from roles: ```typescript @Injectable() export class OwnershipGuard implements CanActivate { constructor(private postsService: PostsService) {} async canActivate(context: ExecutionContext): Promise<boolean> { const { user, params } = context.switchToHttp().getRequest(); if (user.roles.includes(Role.Admin)) return true; // admins bypass const post = await this.postsService.findOne(params.id); return post?.authorId === user.id; } } // Chain it only when you need both checks @Put(':id') @UseGuards(AuthGuard('jwt'), RolesGuard, OwnershipGuard) update(@Param('id') id: string, @Body() dto: UpdatePostDto) { return this.postsService.update(id, dto); } ``` ### RBAC vs ABAC | | RBAC | ABAC | |---|---|---| | Based on | Static roles | Attributes (role, time, IP, resource state) | | Complexity | Low | High | | Flexibility | Enough for most apps | Handles dynamic policies | | Speed | Metadata lookup (microseconds) | Policy scan (milliseconds) | | When to use | CRUD APIs, SaaS dashboards | Enterprise, compliance, AWS IAM-like | The rule of thumb I apply on projects: if you can write all your access rules on a whiteboard in under five minutes, RBAC is enough. If you start drawing arrows between user attributes and resource states, look at Casbin. ### Common mistakes **1. Using `reflector.get` instead of `getAllAndOverride`** ```typescript // Wrong - misses class-level @Roles() entirely const roles = this.reflector.get(ROLES_KEY, context.getHandler()); // Correct const roles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); ``` If you put `@Roles(Role.Admin)` on the controller class and point `reflector.get` only at the handler, every route in that controller is unprotected. This is the most common RBAC bug on Stack Overflow. **2. `RolesGuard` without `AuthGuard` first** ```typescript // Wrong - request.user is undefined, guard throws at runtime @UseGuards(RolesGuard) // Correct @UseGuards(AuthGuard('jwt'), RolesGuard) ``` `AuthGuard('jwt')` runs the Passport strategy that validates the token and sets `request.user`. Without it, `user?.roles` is `undefined` and the guard either throws or silently passes everything. **3. Roles as a string in JWT, not an array** ```typescript // Token: { role: 'admin' } ← breaks multi-role users // Token: { roles: ['admin', 'moderator'] } ← correct ``` When you later need to assign two roles to one user, a single string gives you nowhere to go. Start with an array from day one. **4. Global guard without handling public routes** A global `RolesGuard` with no `@Roles()` on `/health` returns 403 and breaks your deployment health check. The `if (!requiredRoles) return true` line in the Guard handles this automatically. **5. Fetching roles from DB on every request** At 1000 req/s that becomes a bottleneck. Put roles in the JWT payload. If roles change, use a short expiry (15 minutes) and a refresh token that pulls updated roles from DB on rotation. ### Real-world usage - NestJS official docs: Guards + Reflector as the standard RBAC pattern - `@nestjs/passport` + JWT: roles in token payload, validated once, reused by `RolesGuard` - Prisma + NestJS: role queried once at login, stored in token, no DB hit per request - Casbin + `nest-casbin`: when RBAC combinations grow past 10-15, swap to a policy engine - Multi-tenant APIs: add `tenantId` to the role check (`admin:tenant1`), Guard compares `user.tenantId` to `req.tenantId` ### Follow-up questions **Q:** How do you handle role hierarchy where superadmin inherits all admin permissions? **A:** Extend the Guard with a `roleHierarchy` map: `{ superadmin: ['admin', 'moderator', 'user'] }`. In `canActivate`, check `hierarchy[userRole]?.includes(required)` before returning false. Keep the map in memory - no DB lookups needed. **Q:** What happens if a user's roles change mid-session? **A:** Short-lived JWTs (15 minutes) with a refresh token that re-reads roles from DB on each rotation. Avoid long-lived tokens if you need real-time role revocation. **Q:** How do you test `RolesGuard` with Jest? **A:** Mock `Reflector.getAllAndOverride` to return `['admin']`, mock `ExecutionContext` so `switchToHttp().getRequest()` returns `{ user: { roles: ['admin'] } }`, then call `guard.canActivate(mockContext)` and assert the result. Test both match and mismatch cases. This is one of the most common NestJS senior interview questions. **Q:** How do you apply RBAC across microservices? **A:** Put roles in the JWT payload. Each service reads the token independently and runs its own `RolesGuard`. No cross-service role calls needed. When roles change, the refresh flow propagates the update across all services automatically. **Q:** When does RBAC stop working? **A:** When access rules depend on attributes beyond role: time of day, geographic region, resource ownership state. That is when you move to ABAC or a policy engine like Casbin. Pure RBAC handles up to roughly 100 roles cleanly. ## Examples ### Basic: protecting admin routes ```typescript // role.enum.ts export enum Role { User = 'user', Moderator = 'moderator', Admin = 'admin', SuperAdmin = 'superadmin', } // roles.decorator.ts import { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); // roles.guard.ts @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const required = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!required) return true; const { user } = context.switchToHttp().getRequest(); return required.some((role) => user?.roles?.includes(role)); } } // admin.controller.ts @Controller('admin') @UseGuards(AuthGuard('jwt'), RolesGuard) export class AdminController { @Get('dashboard') @Roles(Role.Admin, Role.SuperAdmin) getDashboard() { return { message: 'Admin dashboard' }; } @Delete('users/:id') @Roles(Role.SuperAdmin) // only superadmin can delete users deleteUser(@Param('id') id: string) { return this.usersService.delete(id); } } ``` Admin hits `/admin/dashboard` - 200. Regular user hits the same route - 403. SuperAdmin calls `/admin/users/42` DELETE - 200. Admin tries the same DELETE - 403. ### Intermediate: e-commerce orders with role-conditional logic ```typescript // orders.controller.ts @Controller('orders') @UseGuards(AuthGuard('jwt'), RolesGuard) export class OrdersController { @Get() @Roles(Role.User, Role.Admin) findAll(@Req() req) { // admins see all orders; users see only their own return req.user.roles.includes(Role.Admin) ? this.ordersService.findAll() : this.ordersService.findByUser(req.user.id); } @Patch(':id/refund') @Roles(Role.Admin) // only admins can trigger refunds refund(@Param('id') id: string) { return this.ordersService.refund(id); // Admin → { status: 'refunded' } // Moderator → 403 Forbidden } } ``` The JWT strategy attaches `user.roles` from the token payload. The Guard checks roles before the handler runs. Branching inside the handler on roles is a normal pattern here, not a code smell. ### Advanced: hierarchical roles ```typescript // Hierarchy map (in memory, loaded once at startup) const roleHierarchy: Record<string, string[]> = { superadmin: ['admin', 'moderator', 'user'], admin: ['moderator', 'user'], moderator: ['user'], }; @Injectable() export class HierarchyRolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const required = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!required) return true; const { user } = context.switchToHttp().getRequest(); return required.some((r) => this.hasRole(user.roles, r)); } private hasRole(userRoles: string[], required: string): boolean { if (userRoles.includes(required)) return true; return userRoles.some((r) => roleHierarchy[r]?.includes(required)); } } // superadmin passes @Roles(Role.Admin) automatically @Get('reports') @Roles(Role.Admin) getReports() { ... } // user.roles = ['superadmin'] → true // user.roles = ['moderator'] → false ``` Without the hierarchy check you list every role explicitly: `@Roles(Role.Admin, Role.SuperAdmin)`. With it, write the minimum required role and let inheritance handle the rest.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.