Roles & Permissions (RBAC)

Allow Some Users to Do More Than Others

Roles & Permissions (RBAC)

Combine a custom `@Roles()` decorator with a guard that reads metadata via Reflector. Same pattern scales up to permission-based checks.

4 min read Level 3/5 #nestjs#auth#rbac
What you'll learn
  • Define a @Roles('admin') decorator with SetMetadata
  • Write a RolesGuard that reads it via Reflector
  • Apply globally or per route

Authentication answers who you are. Authorization answers what you can do. Role-Based Access Control is the most common way to model the second question — and a great showcase of Nest’s metadata system.

The @Roles Decorator

SetMetadata attaches arbitrary data to a route handler. A custom decorator just wraps it for readability.

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

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

Now you can write:

@Roles('admin', 'editor')
@Delete(':id')
remove(@Param('id') id: string) { /* ... */ }

The decorator does nothing by itself — it only stores metadata. A guard has to read and act on it.

The RolesGuard

Reflector is the helper for reading metadata. getAllAndOverride walks both the handler and the class, preferring the handler.

import {
  CanActivate, ExecutionContext, Injectable, ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';

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

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

    const { user } = ctx.switchToHttp().getRequest();
    const ok = required.some((r) => user?.roles?.includes(r));
    if (!ok) throw new ForbiddenException();
    return true;
  }
}

If no @Roles metadata is present, the guard lets the request through — public to anyone who passed the auth guard before it.

Register Globally

You almost always want this applied to every route. Use APP_GUARD so you don’t have to remember it on each controller.

@Module({
  providers: [
    { provide: APP_GUARD, useClass: JwtAuthGuard }, // identifies user
    { provide: APP_GUARD, useClass: RolesGuard },   // authorizes user
  ],
})
export class AuthModule {}

Order matters when both run: the JWT guard populates req.user, the roles guard reads it.

Beyond Roles

Pure RBAC gets clumsy fast — you end up with “manager but not for this team” cases. The same pattern scales to permission-based authorization:

@Permissions('user.delete')
@Delete(':id')
remove(/* ... */) {}

Same decorator, same guard shape, but validate checks user.permissions against required permissions. It’s worth the small upfront cost.

OAuth — Sign In With Google →