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:
-
Metadata Collection: The
@UseGuards()decorator stores guard constructors in metadata using theGUARDS_METADATAconstant (packages/common/constants.ts). -
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. Thecreatemethod (lines 22-38) handles this resolution, including global guards registered viagetGlobalMetadata(lines 95-119). -
Execution Loop:
GuardsConsumer.tryActivate(packages/core/guards/guards-consumer.ts, lines 7-35) receives the guard array and callsguard.canActivate(context)sequentially. If any guard returnsfalse, the request is denied immediately. -
Async Normalization: The
pickResultmethod (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 theReflectorservice and other dependencies.Reflector: Reads metadata set by custom decorators (like@Roles()) usingreflect-metadataunder 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:
-
packages/common/interfaces/features/can-activate.interface.ts: Defines theCanActivateinterface contract that all guards must implement (lines 14-25). -
packages/core/guards/guards-context-creator.ts: Contains thecreatemethod (lines 22-38) that resolves guard metadata into instances, andgetGlobalMetadata(lines 95-119) for handling global guard registration. -
packages/core/guards/guards-consumer.ts: ImplementstryActivate(lines 7-35) which executes the guard chain, andpickResult(lines 49-56) for normalizing sync/async return values. -
packages/core/helpers/execution-context-host.ts: Provides theExecutionContextimplementation withswitchToHttp(),getHandler(), andgetClass()methods. -
sample/36-hmr-esm/src/common/guards/roles.guard.ts: Reference implementation of a role-based guard used in NestJS official samples.
Summary
- Implement
CanActivate: All custom guards must implement this interface and return a boolean, Promise, or Observable fromcanActivate(). - Use
@Injectable(): Enable dependency injection for services likeReflectoror custom repositories within your guard. - Leverage
ExecutionContext: Access HTTP requests viacontext.switchToHttp().getRequest()and route metadata viacontext.getHandler(). - Register appropriately: Use
@UseGuards()for route/controller scope, orapp.useGlobalGuards()for application-wide protection (passing dependencies manually when needed). - Understand the pipeline: Guards are resolved by
GuardsContextCreatorand executed byGuardsConsumerbefore 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →