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.
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.