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.

Leave a Comment

Your email address will not be published. Required fields are marked *