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
- What are Guards? — Guards implement CanActivate and return true (allow) or false (deny). They run AFTER middleware but BEFORE interceptors and pipes. Perfect for authentication and authorization — they decide if a request should proceed based on conditions like valid tokens or user roles.
- AuthGuard — JWT Authentication — The most common guard: verify the JWT token from the Authorization header. Extract the user, attach it to the request, and let the handler access it. If the token is invalid or missing, throw UnauthorizedException (401). One guard protects your entire API.
- RolesGuard — Authorization — After authentication (who are you?), authorization checks permissions (what can you do?). RolesGuard reads @Roles('admin') metadata from the route and compares it with the user's role. Admins can delete, users can only read. Separate concerns = clean code.
- Applying Guards — Three levels: @UseGuards(AuthGuard) on a single route, on a controller (all routes), or globally via app.useGlobalGuards(). Global is best for auth — protect everything by default, then whitelist public routes with a @Public() decorator.
- @SetMetadata() & Reflector — Guards read metadata set by decorators. @SetMetadata('roles', ['admin']) attaches data to a route. Inside the guard, Reflector reads it: reflector.get('roles', context.getHandler()). This is how @Roles() and @Public() work under the hood.
- Custom @CurrentUser() Decorator — Create a parameter decorator that extracts the logged-in user from the request: `createParamDecorator((data, ctx) => ctx.switchToHttp().getRequest().user)`. Now use @CurrentUser() in any controller instead of repeating request extraction logic everywhere.
- Custom @Public() Decorator — Mark routes that skip authentication: `const Public = () => SetMetadata('isPublic', true)`. In your AuthGuard, check this metadata — if isPublic is true, return true without checking the token. Login and register routes need this!
- Combining Guards — Stack multiple guards: @UseGuards(AuthGuard, RolesGuard). They run in order — if AuthGuard fails, RolesGuard never runs. This creates a security pipeline: first verify identity, then check permissions. Clean separation of authentication and authorization.
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. JWT Auth Guard — verify tokens
- 2. Import Guard interfaces and exceptions
- 3.
- 4.
- 5.
- 6. Import Reflector to read metadata
- 7. Import JwtService to verify tokens
- 8.
- 9. Injectable guard class
- 10. Constructor with injected dependencies
- 11. JwtService for token verification
- 12. Reflector for reading decorator metadata
- 13. Close constructor
- 14.
- 15. The canActivate method — returns boolean
- 16. Check if route is marked @Public()
- 17. Read 'isPublic' metadata from the handler
- 18. Close reflector call
- 19. If public, allow without checking token
- 20.
- 21. Get the raw HTTP request
- 22. Extract token from 'Bearer ' header
- 23.
- 24. If no token, throw 401
- 25. Throw UnauthorizedException
- 26. Close if
- 27.
- 28. Try to verify the token
- 29. Decode and verify the JWT payload
- 30. Attach user data to the request object
- 31. Allow the request to proceed
- 32. If verification fails...
- 33. Throw invalid token error
- 34. Close try/catch
- 35. Close canActivate
- 36. Close class
- 37.
- 38. 2. Roles Guard — check permissions
- 39. Injectable guard class
- 40. Constructor with Reflector
- 41.
- 42. canActivate checks permissions
- 43. Read required roles from metadata
- 44. Get 'roles' array from the handler
- 45. Close reflector call
- 46. If no roles required, allow all
- 47.
- 48. Extract user from request
- 49. Check if user's role is in the required list
- 50. Close canActivate
- 51. Close class
- 52.
- 53. 3. Custom Decorators
- 54. Import decorator creators
- 55.
- 56. @Public() — sets isPublic metadata to true
- 57.
- 58. @Roles() — sets required roles metadata
- 59. Accept any number of role strings
- 60.
- 61. @CurrentUser() — extract user from request
- 62. createParamDecorator factory
- 63. Get the HTTP request from context
- 64. Get the user attached by AuthGuard
- 65. If 'data' specified, return user[data], else full user
- 66. Close decorator
- 67.
- 68. Usage in controller
- 69. Controller with base route
- 70. Apply both guards to all routes
- 71. Exporting the controller class
- 72. GET /users/profile
- 73. Use @CurrentUser() — clean extraction!
- 74. Return the user object directly
- 75. Close method
- 76.
- 77. DELETE /users/:id — admin only
- 78. Require 'admin' role
- 79. Extract the ID parameter
- 80. Delete the user
- 81. Close method
- 82.
- 83. POST /users/login — public access
- 84. @Public() skips authentication
- 85. Extract login credentials from body
- 86. Delegate to auth service
- 87. Close method
- 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
- NestJS Guards (NestJS Official)
- NestJS Custom Decorators (NestJS Official)
- NestJS Custom Route Decorators (NestJS Official)