NestJS — Enterprise Node.js

Role-Based Access Control (RBAC)

17 min Lesson 29 of 48

Role-Based Access Control (RBAC)

Authentication tells you who a user is; authorization decides what they may do. The most common model is RBAC — Role-Based Access Control — where users have roles (admin, editor, viewer) and routes require specific roles. NestJS implements this elegantly with the guards, metadata, and decorators you already know.

The building blocks

RBAC in NestJS combines three things you met in Phase 3:

  • A custom decorator (@Roles()) to declare which roles a route needs.
  • Metadata attached by that decorator via SetMetadata.
  • A guard that reads the metadata with the Reflector and checks the user's roles.

The Roles decorator

import { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

The RolesGuard

The guard reads the required roles and compares them against the authenticated user's roles (the user was attached by the JWT strategy):

import { Injectable, CanActivate, ExecutionContext } 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 required = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!required) return true; // no roles required -> open const { user } = context.switchToHttp().getRequest(); return required.some((role) => user?.roles?.includes(role)); } }
getAllAndOverride checks both the handler and the controller class, letting a method-level @Roles() override a controller-level one. This is the standard, flexible way to read role metadata.

Putting it together

Protect a route with both guards: first authenticate (JWT), then authorize (roles). Order matters — you must know who the user is before checking their roles:

@UseGuards(AuthGuard('jwt'), RolesGuard) @Roles('admin') @Delete(':id') remove(@Param('id') id: string) { return this.usersService.remove(id); }
Register RolesGuard globally for consistency. Make it an APP_GUARD provider so every route is checked. Routes with no @Roles() are simply allowed, so you opt in to protection per route — and never forget to add the guard.

RBAC vs finer-grained control

RBAC is simple and covers most needs: "admins can delete users." But it struggles with rules that depend on the specific resource — "a user can edit their own post but not others'." That is attribute/ownership-based authorization, which needs more than a role check.

Do not stretch RBAC too far. Encoding ownership rules as ever-more-specific roles (editor-of-post-42) is a trap. When permissions depend on the resource and the actor's relationship to it, move to a policy-based approach — covered next.

Summary

RBAC assigns roles to users and required roles to routes. Implement it with a @Roles() decorator (metadata), a RolesGuard that reads it via the Reflector, and the JWT guard running first so the user is known. Register the guard globally and opt routes in. RBAC handles role-level rules well; for ownership and resource-specific rules, you need policies — the final authorization lesson.