NestJS Role-Based Access
Role-based access control (RBAC) restricts what authenticated users can do based on their assigned role. An admin can delete records; a regular user cannot. A manager can view reports; a viewer cannot. NestJS implements RBAC by combining a custom decorator (to declare required roles on routes) with a guard (to check whether the logged-in user has those roles).
The RBAC Pattern
Route declares required roles → Guard checks user's actual roles → Allow or deny
@Get('reports')
@Roles('admin', 'manager') ← requires admin OR manager role
getReports() { ... }
User role: 'admin' → allowed
User role: 'manager' → allowed
User role: 'user' → 403 Forbidden
Step 1: Define a Role Enum
// role.enum.ts
export enum Role {
User = 'user',
Manager = 'manager',
Admin = 'admin',
}
Using an enum prevents typos. Referring to Role.Admin is safer than the string 'admin' because TypeScript catches invalid values at compile time.
Step 2: Add Role to the User Entity
// user.entity.ts
import { Role } from './role.enum';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ type: 'enum', enum: Role, default: Role.User })
role: Role;
}
Step 3: Create the Roles Decorator
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from './role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
SetMetadata attaches the required roles array to the route handler as metadata. The guard reads this metadata later to know what roles the route demands.
Step 4: Create the Roles Guard
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from './role.enum';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles || requiredRoles.length === 0) {
return true; // no roles required → open to all authenticated users
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some(role => user.role === role);
}
}
Step 5: Apply Both Guards to Routes
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard) // JWT first, then roles
export class AdminController {
@Get('users')
@Roles(Role.Admin)
listAllUsers() { ... }
@Get('reports')
@Roles(Role.Admin, Role.Manager)
getReports() { ... }
@Get('dashboard') // no @Roles → any authenticated user
getDashboard() { ... }
}
Full RBAC Flow Diagram
GET /admin/reports
Authorization: Bearer <token>
|
v
JwtAuthGuard verifies token
→ attaches user { id: 1, role: 'manager' } to req.user
|
v
RolesGuard reads @Roles metadata: ['admin', 'manager']
→ checks req.user.role ('manager')
→ 'manager' is in ['admin', 'manager'] → true
|
v
AdminController.getReports() executes ✓
GET /admin/reports with role 'user':
→ RolesGuard: 'user' not in ['admin', 'manager'] → false → 403 Forbidden
Multiple Roles on One Route
Passing multiple roles to @Roles() means the user must have ANY ONE of those roles. If you need the user to have ALL roles simultaneously, change the some to every in the guard's check:
// ANY one role (default pattern) return requiredRoles.some(role => user.role === role); // ALL roles required return requiredRoles.every(role => user.roles.includes(role));
Public Routes
When you apply JwtAuthGuard globally, every route requires a valid token. Mark public routes with a custom @Public() decorator that the guard reads to skip authentication:
// public.decorator.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// jwt.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) { super(); }
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
[context.getHandler(), context.getClass()],
);
if (isPublic) return true;
return super.canActivate(context);
}
}
// Usage
@Get('health')
@Public()
healthCheck() { return 'OK'; }
Role-based access is the backbone of any multi-user application. By combining decorators, guards, and JWT claims, NestJS makes it straightforward to define fine-grained access rules directly on the routes they govern — readable, centralized, and easy to extend as your permission requirements evolve.
