typescript 83 lines · 3 tabs

Role-Based Access Control in NestJS with a Custom Guard and @Roles Decorator

Shared by codesnips Aug 2026
3 tabs
import { SetMetadata } from '@nestjs/common';

export enum Role {
  User = 'user',
  Editor = 'editor',
  Admin = 'admin',
}

export const ROLES_KEY = 'roles';

export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
3 files · typescript Explain with highlit

This snippet shows the canonical NestJS pattern for role-based access control: attach required roles to a route with a metadata decorator, then read that metadata inside a guard and compare it against the authenticated user. The design keeps authorization declarative at the controller level while centralizing the enforcement logic in one reusable class.

In roles.decorator.ts, a custom @Roles(...) decorator is built on top of SetMetadata. Rather than hardcoding the metadata key as a raw string in multiple places, the key is exported as ROLES_KEY so both the decorator and the guard reference the same constant. The Role enum gives the allowed values a single source of truth, which prevents typos like 'admin' versus 'Admin' from silently disabling a check. SetMetadata simply stores the passed roles array against the route handler and controller class for later retrieval.

In roles.guard.ts, the RolesGuard implements the CanActivate interface and injects Reflector, the NestJS utility for reading metadata. It calls getAllAndOverride with both the handler and the class as targets, which lets a method-level @Roles override a controller-level default. When no roles are attached, the guard returns true, treating the route as public so that adding RBAC is opt-in per route. The request is pulled from the ExecutionContext, and the user (assumed to be populated by an earlier authentication guard such as a JWT strategy) is matched against the required roles with some. Throwing ForbiddenException yields a proper 403 rather than a generic error.

In admin.controller.ts, the guards are composed with @UseGuards(JwtAuthGuard, RolesGuard) so authentication runs before authorization, guaranteeing request.user exists when the role check runs. Ordering matters here: an unauthenticated request should fail with 401 before role logic executes.

The trade-off of this approach is that the guard trusts whatever populated request.user, so it must always be paired with authentication. Registering RolesGuard globally is possible but then every route needs explicit roles or a public marker. Keeping it per-controller, as shown, makes the security surface easy to audit. This pattern is the go-to whenever access depends on a user's role rather than raw authentication.


Related snips

Share this code

Here's the card — post it anywhere.

Role-Based Access Control in NestJS with a Custom Guard and @Roles Decorator — share card
Link copied