Lesson 45 of 49 intermediate

NestJS Guards & Custom Decorators

Security Checkpoints for Your API

Open interactive version (quiz + challenge)

Real-world analogy

Guards are like passport control at an international border. The officer (Guard) checks your passport (JWT token) and visa stamp (role). Diplomats go through the fast lane (admin routes), citizens enter freely (authenticated routes), and travelers without a visa get denied entry. Custom decorators are like visa stamps — small annotations that carry big authorization decisions.

What is it?

Guards are NestJS classes that decide whether a request should be handled or rejected. They implement the CanActivate interface and are used for authentication (verify identity) and authorization (check permissions). Custom decorators create reusable metadata tags and parameter extractors.

Real-world relevance

Every secure API needs guards. Firebase uses them for authentication, Stripe for API key validation, GitHub for OAuth scopes. Role-based access control (RBAC) is standard in enterprise apps — guards make it declarative and clean.

Key points

Code example

// 1. JWT Auth Guard — verify tokens
import {
  Injectable, CanActivate, ExecutionContext,
  UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';

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

  canActivate(context: ExecutionContext): boolean {
    // Check if route is marked @Public()
    const isPublic = this.reflector.get<boolean>(
      'isPublic', context.getHandler(),
    );
    if (isPublic) return true;

    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];

    if (!token) {
      throw new UnauthorizedException('No token provided');
    }

    try {
      const payload = this.jwtService.verify(token);
      request.user = payload; // Attach user to request
      return true;
    } catch {
      throw new UnauthorizedException('Invalid token');
    }
  }
}

// 2. Roles Guard — check permissions
@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; // No roles required

    const { user } = context.switchToHttp().getRequest();
    if (!user) return false; // No authenticated user
    return requiredRoles.includes(user.role);
  }
}

// 3. Custom Decorators
import { SetMetadata, createParamDecorator } from '@nestjs/common';

// @Public() — skip auth for this route
export const Public = () => SetMetadata('isPublic', true);

// @Roles('admin', 'editor') — require specific roles
export const Roles = (...roles: string[]) =>
  SetMetadata('roles', roles);

// @CurrentUser() — extract user from request
export const CurrentUser = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
    return data ? user?.[data] : user;
  },
);

// Usage in controller
@Controller('users')
@UseGuards(AuthGuard, RolesGuard)
export class UsersController {
  constructor(
    private readonly usersService: UsersService,
    private readonly authService: AuthService,
  ) {}

  @Get('profile')
  getProfile(@CurrentUser() user: any) {
    return user; // Clean! No req.user digging
  }

  @Delete(':id')
  @Roles('admin')
  remove(@Param('id') id: string) {
    return this.usersService.remove(id);
  }

  @Post('login')
  @Public()  // Skip auth for login!
  login(@Body() dto: LoginDto) {
    return this.authService.login(dto);
  }
}

Line-by-line walkthrough

  1. 1. 1. JWT Auth Guard — verify tokens
  2. 2. Import Guard interfaces and exceptions
  3. 3.
  4. 4.
  5. 5.
  6. 6. Import Reflector to read metadata
  7. 7. Import JwtService to verify tokens
  8. 8.
  9. 9. Injectable guard class
  10. 10. Constructor with injected dependencies
  11. 11. JwtService for token verification
  12. 12. Reflector for reading decorator metadata
  13. 13. Close constructor
  14. 14.
  15. 15. The canActivate method — returns boolean
  16. 16. Check if route is marked @Public()
  17. 17. Read 'isPublic' metadata from the handler
  18. 18. Close reflector call
  19. 19. If public, allow without checking token
  20. 20.
  21. 21. Get the raw HTTP request
  22. 22. Extract token from 'Bearer ' header
  23. 23.
  24. 24. If no token, throw 401
  25. 25. Throw UnauthorizedException
  26. 26. Close if
  27. 27.
  28. 28. Try to verify the token
  29. 29. Decode and verify the JWT payload
  30. 30. Attach user data to the request object
  31. 31. Allow the request to proceed
  32. 32. If verification fails...
  33. 33. Throw invalid token error
  34. 34. Close try/catch
  35. 35. Close canActivate
  36. 36. Close class
  37. 37.
  38. 38. 2. Roles Guard — check permissions
  39. 39. Injectable guard class
  40. 40. Constructor with Reflector
  41. 41.
  42. 42. canActivate checks permissions
  43. 43. Read required roles from metadata
  44. 44. Get 'roles' array from the handler
  45. 45. Close reflector call
  46. 46. If no roles required, allow all
  47. 47.
  48. 48. Extract user from request
  49. 49. Check if user's role is in the required list
  50. 50. Close canActivate
  51. 51. Close class
  52. 52.
  53. 53. 3. Custom Decorators
  54. 54. Import decorator creators
  55. 55.
  56. 56. @Public() — sets isPublic metadata to true
  57. 57.
  58. 58. @Roles() — sets required roles metadata
  59. 59. Accept any number of role strings
  60. 60.
  61. 61. @CurrentUser() — extract user from request
  62. 62. createParamDecorator factory
  63. 63. Get the HTTP request from context
  64. 64. Get the user attached by AuthGuard
  65. 65. If 'data' specified, return user[data], else full user
  66. 66. Close decorator
  67. 67.
  68. 68. Usage in controller
  69. 69. Controller with base route
  70. 70. Apply both guards to all routes
  71. 71. Exporting the controller class
  72. 72. GET /users/profile
  73. 73. Use @CurrentUser() — clean extraction!
  74. 74. Return the user object directly
  75. 75. Close method
  76. 76.
  77. 77. DELETE /users/:id — admin only
  78. 78. Require 'admin' role
  79. 79. Extract the ID parameter
  80. 80. Delete the user
  81. 81. Close method
  82. 82.
  83. 83. POST /users/login — public access
  84. 84. @Public() skips authentication
  85. 85. Extract login credentials from body
  86. 86. Delegate to auth service
  87. 87. Close method
  88. 88. Close class

Spot the bug

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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>(
      'roles', context.getClass(),
    );
    if (!roles) return true;
    const { user } = context.switchToHttp().getRequest();
    return roles.includes(user.role);
  }
}
Need a hint?
Where should the Reflector read the roles metadata from — the class or the handler?
Show answer
The Reflector is reading from context.getClass() but @Roles() is set on individual methods (handlers), not the class. Fix: use context.getHandler() to read method-level metadata. Or use reflector.getAllAndOverride() to check both handler AND class metadata for maximum flexibility.

Explain like I'm 5

Guards are like the password on your phone. Before you can use any app (route), you need to unlock it (authenticate). Some apps need extra permission — like your parents' approval to download games (role check). Custom decorators are like name tags at a party — they tell everyone who you are without you having to repeat yourself!

Fun fact

The decorator pattern (@) comes from Python, was adopted by TypeScript via a TC39 proposal, and is now fundamental to Angular, NestJS, and MobX. NestJS uses over 60 built-in decorators — probably more than any other Node.js framework!

Hands-on challenge

Implement a complete auth system: 1) AuthGuard that verifies JWT tokens, 2) RolesGuard that checks user roles, 3) @Public() decorator for login/register, 4) @CurrentUser() decorator to extract the user, 5) @Roles('admin') decorator. Apply AuthGuard globally and test all scenarios!

More resources

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook