NestJS Guards
A guard decides whether a request should proceed to the route handler or be blocked. Guards run after middleware but before interceptors, pipes, and the controller method. They are the standard tool in NestJS for authentication and authorization — checking whether a user is logged in and whether they have permission to perform a specific action.
Guard vs Middleware for Auth
Both guards and middleware can protect routes. Guards are preferred for authentication because they have access to the execution context — they know which controller and method are about to handle the request. This lets guards read custom metadata (like required roles) that decorators attach to route handlers.
Middleware (runs before routing): - Does not know which controller will handle the request - Cannot read custom decorator metadata - Good for broad request preprocessing Guard (runs after routing, before handler): - Knows exactly which controller and method will run - Reads custom metadata via Reflector - Returns true (allow) or false (throw 403)
Creating a Guard
A guard is a class decorated with @Injectable() that implements CanActivate:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers['authorization'];
if (!token) {
return false; // blocks the request (NestJS throws 403)
}
// Validate token here
return true; // allows the request to proceed
}
}
The canActivate method returns true to allow the request or false (or throws an exception) to block it. When it returns false, NestJS automatically throws a ForbiddenException (403).
Applying Guards
// On a single route
@Get('profile')
@UseGuards(AuthGuard)
getProfile() { ... }
// On an entire controller
@Controller('admin')
@UseGuards(AuthGuard)
export class AdminController { ... }
// Globally (every route in the application)
app.useGlobalGuards(new AuthGuard());
Guard Execution Flow
GET /admin/dashboard
|
v
AuthGuard.canActivate()
|
┌─────┴──────────────────┐
│ token present? │
│ YES → return true │
│ NO → return false │
└─────┬──────────────────┘
|
true | false
| |
v v
Controller 403 Forbidden
method
Role-Based Guard Using Custom Metadata
Guards become powerful when combined with custom decorators that attach metadata to routes. First, create a decorator that sets required roles:
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// Apply it to a route
@Get('reports')
@Roles('admin', 'manager')
getReports() { ... }
Then read that metadata inside the guard:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@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 → allow all
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return requiredRoles.some(role => user?.roles?.includes(role));
}
}
Throwing Custom Exceptions from Guards
Instead of returning false (which gives a generic 403), throw a specific exception with a clear message:
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers['authorization'];
if (!token) {
throw new UnauthorizedException('Missing authentication token');
}
if (!this.isValidToken(token)) {
throw new UnauthorizedException('Invalid or expired token');
}
return true;
}
Async Guards
Guards can be asynchronous. Return a Promise<boolean> or Observable<boolean> when the check requires a database lookup or external API call:
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = request.headers['authorization']?.split(' ')[1];
const user = await this.authService.validateToken(token);
if (!user) return false;
request.user = user; // attach the user for use in controllers
return true;
}
Attaching the validated user to request.user is the standard pattern. Your controllers can then access the authenticated user with @Req() req or through a custom @CurrentUser() decorator — removing the need to validate the token again in the service layer.
