How to Implement Custom Guards in NestJS: Authorization and Access Control

To implement custom guards in NestJS, create an @Injectable() class that implements the CanActivate interface, define authorization logic in the canActivate method to return a boolean (or Promise/Observable), and apply the guard using @UseGuards() at the route or controller level, or app.useGlobalGuards() for application-wide protection.

Guards in NestJS act as the authorization gatekeepers of the request pipeline, determining whether incoming requests proceed to route handlers based on custom logic such as roles, permissions, or feature flags. According to the nestjs/nest source code, guards are first-class injectable classes that execute before interceptors and route handlers, leveraging the framework's dependency injection container for complex authentication scenarios. This guide examines the actual implementation details from the repository to build production-ready authorization guards.

Guard Architecture and Execution Flow

NestJS executes guards through a well-defined pipeline involving metadata collection, dependency resolution, and sequential activation. Understanding this flow helps debug authorization issues and optimize guard performance.

The CanActivate Contract

Every guard must implement the CanActivate interface defined in packages/common/interfaces/features/can-activate.interface.ts. The contract requires a single method:

canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>

This method receives an ExecutionContext instance (implemented in packages/core/helpers/execution-context-host.ts) that provides access to the underlying HTTP request via context.switchToHttp().getRequest() and route handler metadata via context.getHandler().

Guard Resolution and Activation

When a request enters the pipeline, the framework executes guards through several internal components:

  1. Metadata Collection: The @UseGuards() decorator stores guard constructors in metadata using the GUARDS_METADATA constant (packages/common/constants.ts).

  2. Context Creation: GuardsContextCreator (packages/core/guards/guards-context-creator.ts) reads this metadata and resolves each guard to an instance, either instantiating new objects or retrieving them from the DI container. The create method (lines 22-38) handles this resolution, including global guards registered via getGlobalMetadata (lines 95-119).

  3. Execution Loop: GuardsConsumer.tryActivate (packages/core/guards/guards-consumer.ts, lines 7-35) receives the guard array and calls guard.canActivate(context) sequentially. If any guard returns false, the request is denied immediately.

  4. Async Normalization: The pickResult method (lines 49-56) handles synchronous booleans, Promises, and RxJS Observables uniformly, allowing guards to use any async pattern.

Implementing a Custom Roles Guard

Below is a production-ready implementation that checks if an authenticated user possesses required roles. This example follows the pattern used in sample/36-hmr-esm/src/common/guards/roles.guard.ts.

// src/common/guards/roles.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from '../decorators/roles.decorator';

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

  canActivate(context: ExecutionContext): boolean {
    // Read metadata attached by the @Roles() decorator
    const requiredRoles = this.reflector.get<string[]>(Roles, context.getHandler());
    
    if (!requiredRoles?.length) {
      return true;
    }

    // Extract user from request (assumes prior auth middleware)
    const request = context.switchToHttp().getRequest();
    const user = request.user;

    // Check if user has at least one required role
    return !!user?.roles?.some(role => requiredRoles.includes(role));
  }
}

Key implementation details:

  • @Injectable(): Required for NestJS to inject the Reflector service and other dependencies.
  • Reflector: Reads metadata set by custom decorators (like @Roles()) using reflect-metadata under the hood.
  • context.switchToHttp(): Provides access to the underlying Express or Fastify request object.
  • Return value: Must resolve to a truthy value to allow the request; falsy values trigger a 403 Forbidden response.

The accompanying metadata decorator (referenced in sample/10-fastify/src/common/decorators/roles.decorator.ts) attaches the roles array to the route handler:

import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

Registering Guards at Different Scopes

NestJS allows guard registration at three levels of granularity, each suited to different authorization patterns.

Route-Level Guards

Apply guards to individual handlers using the @UseGuards() decorator:

import { UseGuards, Get } from '@nestjs/common';
import { RolesGuard } from '../guards/roles.guard';

@Controller('admin')
export class AdminController {
  @Roles('admin', 'manager')
  @UseGuards(RolesGuard)
  @Get('dashboard')
  getDashboard() {
    return 'Sensitive admin data';
  }
}

Controller-Level Guards

To protect every route within a controller, apply the decorator at the class level:

@UseGuards(RolesGuard)
@Controller('admin')
export class AdminController {
  // All routes inherit this guard
}

Global Guards

For application-wide authorization (such as JWT validation), register guards during bootstrap:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RolesGuard } from './common/guards/roles.guard';
import { Reflector } from '@nestjs/core';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // Manual instantiation requires passing dependencies explicitly
  app.useGlobalGuards(new RolesGuard(app.get(Reflector)));
  
  await app.listen(3000);
}
bootstrap();

Important: When using useGlobalGuards() with manually instantiated guards, you must explicitly pass dependencies like Reflector because the global registration bypasses the automatic dependency injection that occurs with @UseGuards().

Key Source Files in the NestJS Repository

Understanding these source files helps when debugging guard behavior or extending the framework:

Summary

  • Implement CanActivate: All custom guards must implement this interface and return a boolean, Promise, or Observable from canActivate().
  • Use @Injectable(): Enable dependency injection for services like Reflector or custom repositories within your guard.
  • Leverage ExecutionContext: Access HTTP requests via context.switchToHttp().getRequest() and route metadata via context.getHandler().
  • Register appropriately: Use @UseGuards() for route/controller scope, or app.useGlobalGuards() for application-wide protection (passing dependencies manually when needed).
  • Understand the pipeline: Guards are resolved by GuardsContextCreator and executed by GuardsConsumer before any interceptor or route handler runs.

Frequently Asked Questions

What is the CanActivate interface in NestJS?

The CanActivate interface is the contract that defines a NestJS guard, located in packages/common/interfaces/features/can-activate.interface.ts. It requires a single canActivate(context: ExecutionContext) method that returns a boolean, Promise, or Observable. When this method returns true (or resolves to true), the request proceeds; otherwise, NestJS returns a 403 Forbidden error.

How do I inject dependencies into a custom guard?

Decorate your guard class with @Injectable() and include dependencies in the constructor. NestJS's dependency injection container automatically resolves these when the guard is applied via @UseGuards(). However, when using app.useGlobalGuards() with manual instantiation (e.g., new RolesGuard()), you must explicitly pass dependencies using app.get(Reflector) or other provider retrieval methods.

Can guards be applied globally in NestJS?

Yes, guards can be registered globally using app.useGlobalGuards() in your bootstrap function, or by setting APP_GUARD as a provider in a module. Global guards execute for every route in the application, making them ideal for authentication or universal access logging. Note that global guards run after middleware but before route-specific guards.

How does NestJS handle asynchronous guard results?

The GuardsConsumer class in packages/core/guards/guards-consumer.ts normalizes all return types through the pickResult method (lines 49-56), which supports plain booleans, Promises, and RxJS Observables. This allows you to implement async logic—such as database lookups or external API calls—within canActivate() using standard async/await or Observable patterns without additional configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →