Guards, Metadata & the Reflector
Attach roles and permissions with custom decorators, then read them in a guard using Reflector.getAllAndOverride.
Authorization means comparing what a handler requires against what the caller has. Nest models the requirement as metadata attached to the route, and guards read it back with the Reflector.
Attaching metadata with a decorator
SetMetadata(key, value) tags a handler or controller. Wrap it in a named decorator so intent is obvious at the call site:
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) =>
SetMetadata(ROLES_KEY, roles);
// @Roles('admin') on a handlerReading it back correctly
A guard injects Reflector and reads the metadata. The key detail seniors get right is which lookup method to use:
getAllAndOverride(key, [handler, class])- handler metadata overrides class metadata. Use this for roles: a method can tighten what the controller declared.getAllAndMerge(key, [handler, class])- combines both into one array. Use this when permissions accumulate.
Return true for public routes
If no metadata is present, decide your default. A common pattern is an @Public() decorator that a global JwtAuthGuard checks first, so most routes are protected and opting out is explicit.
Guards are the auth layer
Because guards run before pipes and interceptors and have full access to the ExecutionContext, they are the correct home for every allow/deny decision. Keep business rules out of them - a guard answers "may this request proceed?" and nothing more.
Example
import {
Injectable,
CanActivate,
ExecutionContext,
SetMetadata,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
export const IS_PUBLIC = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC, true);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
// Public routes bypass the check entirely.
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC, [
ctx.getHandler(),
ctx.getClass(),
]);
if (isPublic) return true;
// Handler-level @Roles overrides controller-level @Roles.
const required = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
ctx.getHandler(),
ctx.getClass(),
]);
if (!required || required.length === 0) return true;
const { user } = ctx.switchToHttp().getRequest();
const ok = required.some((role) => user?.roles?.includes(role));
if (!ok) {
throw new ForbiddenException(
`Requires one of: ${required.join(', ')}`,
);
}
return true;
}
}
// Usage:
// @Roles('admin')
// @Delete(':id')
// remove(@Param('id') id: string) { ... }When to use it
- A developer uses @SetMetadata("permissions", ["read:users"]) to attach permission strings to routes and reads them in a PermissionsGuard to enforce fine-grained access control.
- A team uses Reflector.getAllAndOverride to read metadata from the handler first and fall back to class-level metadata, letting controller-wide defaults be overridden per route.
- An engineer combines @Public() and a global JWT guard so all routes require auth by default, with specific routes opted out via metadata.
More examples
Custom metadata decorator
Creates a @RequirePermissions decorator that attaches an array of permission strings as metadata on a handler using SetMetadata.
import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...perms: string[]) =>
SetMetadata(PERMISSIONS_KEY, perms);
// Usage on a route:
@RequirePermissions('users:write', 'users:delete')
@Delete(':id')
remove(@Param('id') id: string) {}Guard reading metadata with Reflector
Reads PERMISSIONS_KEY metadata from the handler first (then the class) and verifies the authenticated user has all required permissions.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PERMISSIONS_KEY } from './permissions.decorator';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<string[]>(
PERMISSIONS_KEY,
[ctx.getHandler(), ctx.getClass()],
);
if (!required?.length) return true;
const { user } = ctx.switchToHttp().getRequest();
return required.every(p => user?.permissions?.includes(p));
}
}Override class metadata per method
Shows how getAllAndOverride prioritises method-level metadata over class-level metadata, enabling per-route permission overrides.
import { Controller, Get, Delete } from '@nestjs/common';
@Controller('articles')
@RequirePermissions('articles:read') // default for all methods
export class ArticlesController {
@Get()
findAll() {} // inherits articles:read
@Delete(':id')
@RequirePermissions('articles:delete') // override for this method
remove(@Param('id') id: string) {}
}
Discussion