Ave, brave legionaries! Consul Caesar.js leads you further into the secrets of NestJS. Today we'll learn about Guards - the highest-class guards of our forum who decide who can access the most valuable resources.
Guards in NestJS are like elite treasury guards - they don't just check identity, they make strategic access decisions based on business logic, user roles, and request context.
Guards are classes implementing the
CanActivate interface that determine whether a request can be processed by the handler. They run after middleware but before interceptors and pipes.1import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2
3@Injectable()
4export class LegionGuard implements CanActivate {
5 canActivate(context: ExecutionContext): boolean {
6 const request = context.switchToHttp().getRequest();
7 const legionaryRank = request.headers['legionary-rank'];
8
9 // Only legionaries with rank "centurion" or higher
10 return legionaryRank === 'centurion' || legionaryRank === 'tribune';
11 }
12}The most important guard - checks whether the legionary is a member of the legion:
1import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
2import { JwtService } from '@nestjs/jwt';
3
4@Injectable()
5export class AuthGuard implements CanActivate {
6 constructor(private jwtService: JwtService) {}
7
8 canActivate(context: ExecutionContext): boolean {
9 const request = context.switchToHttp().getRequest();
10 const token = this.extractTokenFromHeader(request);
11
12 if (!token) {
13 throw new UnauthorizedException('No token! Access only for legion members!');
14 }
15
16 try {
17 const payload = this.jwtService.verify(token);
18 request['legionary'] = payload;
19 return true;
20 } catch {
21 throw new UnauthorizedException('Invalid token! Are you sure you are one of us?');
22 }
23 }
24
25 private extractTokenFromHeader(request: any): string | undefined {
26 const [type, token] = request.headers.authorization?.split(' ') ?? [];
27 return type === 'Bearer' ? token : undefined;
28 }
29}The rank and permission system in the Roman legion:
1// decorators/roles.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4export enum LegionaryRank {
5 MILES = 'miles',
6 TESSERARIUS = 'tesserarius',
7 OPTIO = 'optio',
8 CENTURION = 'centurion',
9 TRIBUNE = 'tribune'
10}
11
12export const Ranks = (...ranks: LegionaryRank[]) => SetMetadata('ranks', ranks);
13
14// guards/roles.guard.ts
15import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
16import { Reflector } from '@nestjs/core';
17
18@Injectable()
19export class RolesGuard implements CanActivate {
20 constructor(private reflector: Reflector) {}
21
22 canActivate(context: ExecutionContext): boolean {
23 const requiredRanks = this.reflector.getAllAndOverride<LegionaryRank[]>('ranks', [
24 context.getHandler(),
25 context.getClass(),
26 ]);
27
28 if (!requiredRanks) {
29 return true; // No required ranks = access for everyone
30 }
31
32 const request = context.switchToHttp().getRequest();
33 const legionary = request.legionary;
34
35 if (!legionary) {
36 return false;
37 }
38
39 return requiredRanks.includes(legionary.rank);
40 }
41}
42
43// Usage in controller
44@Controller('tributes')
45export class TributesController {
46 @Get('centurion-only')
47 @Ranks(LegionaryRank.CENTURION, LegionaryRank.TRIBUNE)
48 @UseGuards(AuthGuard, RolesGuard)
49 getCenturionTributes() {
50 return {
51 message: 'Centurion tributes - the most precious gems!',
52 tributes: ['Caesar's Crown', 'Golden Eagle', 'Imperator's Insignia']
53 };
54 }
55}An advanced guard checking various access conditions:
1@Injectable()
2export class TributeAccessGuard implements CanActivate {
3 constructor(
4 private tributeService: TributeService,
5 private legionService: LegionService
6 ) {}
7
8 async canActivate(context: ExecutionContext): Promise<boolean> {
9 const request = context.switchToHttp().getRequest();
10 const legionary = request.legionary;
11 const tributeId = request.params.id;
12
13 // Check if the tribute exists
14 const tribute = await this.tributeService.findById(tributeId);
15 if (!tribute) {
16 throw new NotFoundException('Tribute was not found!');
17 }
18
19 // Different access levels
20 switch (tribute.securityLevel) {
21 case 'public':
22 return true; // Accessible to everyone
23
24 case 'legion':
25 return this.isLegionMember(legionary);
26
27 case 'officers':
28 return this.isOfficer(legionary);
29
30 case 'centurion':
31 return this.isCenturion(legionary);
32
33 case 'personal':
34 return tribute.ownerId === legionary.id;
35
36 default:
37 return false;
38 }
39 }
40
41 private isLegionMember(legionary: any): boolean {
42 return legionary && legionary.status === 'active';
43 }
44
45 private isOfficer(legionary: any): boolean {
46 const officerRanks = [
47 LegionaryRank.OPTIO,
48 LegionaryRank.CENTURION,
49 LegionaryRank.TRIBUNE
50 ];
51 return this.isLegionMember(legionary) && officerRanks.includes(legionary.rank);
52 }
53
54 private isCenturion(legionary: any): boolean {
55 return this.isLegionMember(legionary) &&
56 [LegionaryRank.CENTURION, LegionaryRank.TRIBUNE].includes(legionary.rank);
57 }
58}A guard controlling access depending on time:
1@Injectable()
2export class TimeBasedGuard implements CanActivate {
3 canActivate(context: ExecutionContext): boolean {
4 const now = new Date();
5 const hour = now.getHours();
6 const dayOfWeek = now.getDay();
7
8 const request = context.switchToHttp().getRequest();
9 const route = request.route.path;
10
11 // Treasury accessible only during daytime hours
12 if (route.includes('/vault')) {
13 if (hour < 8 || hour > 18) {
14 throw new ForbiddenException(
15 'Treasury closed! Available only from 8:00 to 18:00'
16 );
17 }
18 }
19
20 // Baths closed on Sundays
21 if (route.includes('/thermae') && dayOfWeek === 0) {
22 throw new ForbiddenException(
23 'Baths closed on Sundays! Even legionaries need rest!'
24 );
25 }
26
27 // Night watch - restricted access after 22:00
28 if (hour >= 22 || hour <= 6) {
29 const legionary = request.legionary;
30 if (!legionary.nightShiftAccess) {
31 throw new ForbiddenException(
32 'Night access only for the night watch!'
33 );
34 }
35 }
36
37 return true;
38 }
39}Access control based on IP address:
1@Injectable()
2export class IpWhitelistGuard implements CanActivate {
3 private readonly allowedIps = [
4 '192.168.1.100', // Consul's computer
5 '192.168.1.101', // Centurion's computer
6 '10.0.0.50', // Engineering server
7 '127.0.0.1' // Localhost for development
8 ];
9
10 canActivate(context: ExecutionContext): boolean {
11 const request = context.switchToHttp().getRequest();
12 const clientIp = this.getClientIp(request);
13
14 if (!this.allowedIps.includes(clientIp)) {
15 console.log(`⚠️ Access attempt from unauthorized IP: ${clientIp}`);
16 throw new ForbiddenException(
17 'Access from this province is not allowed! Only trusted locations can connect!'
18 );
19 }
20
21 return true;
22 }
23
24 private getClientIp(request: any): string {
25 return request.headers['x-forwarded-for'] ||
26 request.connection.remoteAddress ||
27 request.socket.remoteAddress ||
28 request.ip;
29 }
30}Access control for new features:
1@Injectable()
2export class FeatureFlagGuard implements CanActivate {
3 constructor(private configService: ConfigService) {}
4
5 canActivate(context: ExecutionContext): boolean {
6 const handler = context.getHandler();
7 const featureFlag = this.reflector.get<string>('feature', handler);
8
9 if (!featureFlag) {
10 return true; // No flag = unrestricted access
11 }
12
13 const isEnabled = this.configService.get<boolean>(`features.${featureFlag}`);
14
15 if (!isEnabled) {
16 throw new ForbiddenException(
17 `Feature '${featureFlag}' is currently unavailable. Engineers are still working on it!`
18 );
19 }
20
21 return true;
22 }
23}
24
25// Decorator for feature flags
26export const Feature = (flag: string) => SetMetadata('feature', flag);
27
28// Usage
29@Controller('experimental')
30export class ExperimentalController {
31 @Get('new-weapon')
32 @Feature('advanced-ballistae')
33 @UseGuards(FeatureFlagGuard)
34 getNewWeapon() {
35 return {
36 name: 'Quantum Ballista',
37 damage: 9999,
38 status: 'Experimental'
39 };
40 }
41}A guard controlling request frequency:
1@Injectable()
2export class RateLimitGuard implements CanActivate {
3 private requests = new Map<string, { count: number; resetTime: number }>();
4
5 constructor(
6 @Inject('RATE_LIMIT_OPTIONS')
7 private options: { max: number; windowMs: number }
8 ) {}
9
10 canActivate(context: ExecutionContext): boolean {
11 const request = context.switchToHttp().getRequest();
12 const key = this.generateKey(request);
13 const now = Date.now();
14
15 let userRequests = this.requests.get(key);
16
17 if (!userRequests || now > userRequests.resetTime) {
18 userRequests = {
19 count: 0,
20 resetTime: now + this.options.windowMs
21 };
22 }
23
24 userRequests.count++;
25 this.requests.set(key, userRequests);
26
27 if (userRequests.count > this.options.max) {
28 throw new TooManyRequestsException(
29 'Too many requests! The forum cannot withstand such an assault!'
30 );
31 }
32
33 return true;
34 }
35
36 private generateKey(request: any): string {
37 const legionary = request.legionary;
38 return legionary ? `legionary-${legionary.id}` : `ip-${request.ip}`1 }
2}A guard combining multiple conditions:
1@Injectable()
2export class TributeVaultGuard implements CanActivate {
3 constructor(
4 private tributeService: TributeService,
5 private auditService: AuditService
6 ) {}
7
8 async canActivate(context: ExecutionContext): Promise<boolean> {
9 const request = context.switchToHttp().getRequest();
10 const legionary = request.legionary;
11 const tributeId = request.params.id;
12
13 // Condition 1: Check legionary rank
14 const hasRequiredRank = this.checkRank(legionary);
15 if (!hasRequiredRank) {
16 this.auditService.logUnauthorizedAccess(legionary.id, 'insufficient_rank');
17 return false;
18 }
19
20 // Condition 2: Check if legionary has active access
21 const hasActiveAccess = await this.checkActiveAccess(legionary);
22 if (!hasActiveAccess) {
23 this.auditService.logUnauthorizedAccess(legionary.id, 'inactive_access');
24 return false;
25 }
26
27 // Condition 3: Check daily limit
28 const withinDailyLimit = await this.checkDailyLimit(legionary);
29 if (!withinDailyLimit) {
30 this.auditService.logUnauthorizedAccess(legionary.id, 'daily_limit_exceeded');
31 throw new ForbiddenException(
32 'Daily tribute access limit exceeded!'
33 );
34 }
35
36 // Condition 4: Check if tribute is currently in use
37 const tributeAvailable = await this.checkTributeAvailability(tributeId);
38 if (!tributeAvailable) {
39 throw new ConflictException(
40 'Tribute is currently being used by another legionary!'
41 );
42 }
43
44 // Log successful access
45 this.auditService.logSuccessfulAccess(legionary.id, tributeId);
46
47 return true;
48 }
49
50 private checkRank(legionary: any): boolean {
51 return [LegionaryRank.OPTIO, LegionaryRank.CENTURION, LegionaryRank.TRIBUNE]
52 .includes(legionary.rank);
53 }
54
55 private async checkActiveAccess(legionary: any): Promise<boolean> {
56 // Check in the database if the legionary has active access
57 return legionary.status === 'active' && !legionary.suspended;
58 }
59
60 private async checkDailyLimit(legionary: any): Promise<boolean> {
61 const today = new Date().toDateString();
62 const accessCount = await this.auditService.getDailyAccessCount(legionary.id, today);
63 return accessCount < 5; // Maximum 5 accesses per day
64 }
65
66 private async checkTributeAvailability(tributeId: string): Promise<boolean> {
67 return await this.tributeService.isAvailable(tributeId);
68 }
69}1// main.ts
2import { NestFactory } from '@nestjs/core';
3import { AppModule } from './app.module';
4
5async function bootstrap() {
6 const app = await NestFactory.create(AppModule);
7
8 // Global guard for the entire application
9 app.useGlobalGuards(new AuthGuard());
10
11 await app.listen(3000);
12}
13bootstrap();
14
15// Or in a module
16@Module({
17 providers: [
18 {
19 provide: APP_GUARD,
20 useClass: AuthGuard,
21 },
22 ],
23})
24export class AppModule {}