Skip to content

NestJS Role-Based Access Control

Once a user is authenticated, you often need to restrict actions based on their role, admin versus regular user, for example. This lesson builds a role-based access control (RBAC) system using metadata and a guard.

Declaring Required Roles With a Custom Decorator

A @Roles() decorator attaches metadata to a route handler describing which roles are allowed to access it. A RolesGuard then reads this metadata using Reflector and compares it against the authenticated user's role.

import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

SetMetadata() is the low-level building block behind most custom decorators in NestJS, it attaches arbitrary metadata to a class or method that a guard, interceptor, or pipe can later read.

The Matching RolesGuard

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}
  • getAllAndOverride() checks method-level metadata first, falling back to class-level metadata.
  • If no roles are declared for a route, the guard allows the request through by default.
  • The guard assumes request.user was already populated by an earlier auth guard.
  • Combine both guards with @UseGuards(AuthGuard, RolesGuard) in that order.

RBAC Cheatsheet

The pieces that make up a role-based access system.

Piece Role
@Roles('admin') Declares which roles may access a route
SetMetadata() Attaches metadata used by @Roles()
Reflector Reads metadata attached to a handler or class
RolesGuard Compares required roles to the user's actual roles

Applying Roles to Routes

With the decorator and guard in place, protecting a route by role becomes a one-line addition, without touching the guard's implementation for every new role combination.

@Roles('admin')
@UseGuards(AuthGuard, RolesGuard)
@Delete(':id')
remove(@Param('id') id: string) {
  return this.catsService.remove(id);
}

Roles vs. Fine-Grained Permissions

Simple role checks (admin, user) work well for small applications, but larger systems often need finer-grained permissions (cats:delete, cats:write) assigned to roles, letting you change what a role can do without redeploying code.

Common Mistakes

  • Checking roles with strict equality instead of .includes()/.some() against an array of user roles.
  • Forgetting to apply AuthGuard before RolesGuard, leaving request.user undefined.
  • Hardcoding role strings across many files instead of centralizing them as constants or an enum.
  • Not defaulting to "allow" when a route has no @Roles() metadata, accidentally locking out unrelated routes.

Key Takeaways

  • @Roles() and SetMetadata() attach role requirements directly to route handlers.
  • Reflector lets a guard read that metadata at runtime.
  • RolesGuard compares required roles against the authenticated user's roles.
  • Larger systems often evolve role checks into more fine-grained permission systems.

Pro Tip

Define role names as a shared enum or constant object (e.g. Role.Admin) instead of raw strings scattered across @Roles() calls, a typo in a hardcoded role string fails silently and is one of the sneakiest authorization bugs to track down.