NestJS Guards Guide

NestJS guards determine whether a request should be handled. Learn JWT guards, role-based access, @UseGuards decorator, and global guard registration.

1. JWT Auth Guard

import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(
    private jwtService: JwtService,
    private configService: ConfigService,
    private reflector: Reflector,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Check for @Public() decorator
    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;

    const request = context.switchToHttp().getRequest();
    const token = this.extractToken(request);

    if (!token) throw new UnauthorizedException('Missing token');

    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.get('JWT_SECRET'),
      });
      request['user'] = payload;
      return true;
    } catch {
      throw new UnauthorizedException('Invalid token');
    }
  }

  private extractToken(request: Request): string | undefined {
    const [type, token] = request.headers['authorization']?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

2. Role-Based Guard

import { SetMetadata } from '@nestjs/common';

// Decorator to set required roles
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// Public route decorator
export const Public = () => SetMetadata('isPublic', true);

// Role guard
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles || requiredRoles.length === 0) return true;

    const { user } = context.switchToHttp().getRequest();
    if (!user) return false;

    // user.roles is an array
    return requiredRoles.some(role => user.roles?.includes(role));
  }
}

// Controller usage
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
export class AdminController {

  @Get('users')
  getUsers() { ... }

  @Delete('users/:id')
  @Roles('admin', 'superadmin')  // override class-level
  deleteUser(@Param('id') id: string) { ... }

  @Get('health')
  @Public()          // skip auth entirely
  health() { return 'ok'; }
}

3. Global Guard Registration

// app.module.ts — register globally
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

// All routes now protected by default
// Use @Public() to opt-out specific routes

// Alternative: main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new JwtAuthGuard(jwtService, configService, reflector));

4. Passport Integration

// JWT strategy
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    return { id: payload.sub, email: payload.email, roles: payload.roles };
  }
}

// Use with @UseGuards(AuthGuard('jwt'))
@Get('profile')
@UseGuards(AuthGuard('jwt'))
getProfile(@Request() req) {
  return req.user;
}

5. API Key Guard

@Injectable()
export class ApiKeyGuard implements CanActivate {
  constructor(private readonly apiKeysService: ApiKeysService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const apiKey = request.headers['x-api-key'] ?? request.query.api_key;

    if (!apiKey) throw new UnauthorizedException('API key required');

    const key = await this.apiKeysService.validate(apiKey);
    if (!key) throw new UnauthorizedException('Invalid API key');

    request['apiKey'] = key;
    return true;
  }
}

@Controller('api/v1')
@UseGuards(ApiKeyGuard)
export class ApiController { ... }

6. Guard Execution Order

LevelHow to ApplyPriority
GlobalAPP_GUARD or useGlobalGuardsLowest scope, first executed
Controller@UseGuards on classMiddle
Method@UseGuards on methodHighest specificity