NestJS Validating Roles Decorator - Reflection & Metadata
Overview
NestJS leverages decorators, metadata, and reflection to allow developers to add extra information to classes and methods. This added information can then be used by various components (like guards or interceptors) to modify how a request is handled. A common use-case is implementing role-based access control, where specific routes are restricted to users with particular roles.
Creating a Custom Decorator for Roles
To create a custom decorator that attaches role information as metadata:
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
- Role names (e.g., 'admin', 'user') are passed as arguments.
- The SetMetadata function assigns these values to the metadata key 'roles'.
Using the Decorator in a Controller
// users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { Roles } from './roles.decorator';
@Controller('users')
export class UsersController {
@Get()
@Roles('admin')
findAll() {
return "This route is only for admins";
}
}- The @Roles('admin') decorator attaches metadata indicating that access is restricted to users with the admin role.
Implementing a Roles Guard
To enforce role-based access control using a guard that retrieves metadata via reflection:
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return requiredRoles.some(role => user.roles?.includes(role));
}
}
- The Reflector retrieves the 'roles' metadata from the route handler.
- The guard compares required roles with those assigned to the authenticated user and grants access if a match is found.
Integrating the Guard in a Controller
To enforce the role-based guard on a controller or specific route:
// users.controller.ts (updated)
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
@Controller('users')
@UseGuards(RolesGuard)
export class UsersController {
@Get()
@Roles('admin')
findAll() {
return "This route is only for admins";
}
}- The guard is applied at the controller level, ensuring all routes are protected by the role-based restrictions defined in the metadata.
