Guards
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against NestJS 12.1 (npm @nestjs/core 12.1.2, published 2026-09-30), Node.js 22 and 24 LTS,
TypeScript 6 (6.0.x), Express 5.2 and Fastify 5.12, all current as of 2026-10-02. The examples use a Nest 12 project with Vitest and
supertest, and the HTTP behaviour is the same on the Fastify adapter. A guard is the component that answers one question before a handler runs: may this request proceed? It is the building block
behind route protection in the Bookshelf application that grows across this section: readers can list books, editors
can add them, and only admins can delete them. The full authentication and authorization story (sessions, JWT, policies)
belongs to the security pages; this page covers the guard mechanism itself.
What a guard is
A guard is a class decorated with @Injectable() that implements the CanActivate interface. It has a single
responsibility: decide, from conditions that exist at runtime (a role, a permission, a feature flag, a header), whether
the route handler will run. Guards are a Nest core feature (@nestjs/common and @nestjs/core), independent of the HTTP
platform, so the same class can protect a REST controller, a GraphQL resolver, a WebSocket gateway or a microservice
handler.
The interface has one method:
export interface CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>;
}
Source: NestJS: Guards
Nest uses the return value as follows:
-
true: the request continues to the next guard, then to interceptors, pipes and the handler. -
false: Nest throws aForbiddenException, which the exceptions layer turns into a403 Forbiddenresponse. -
An exception thrown from
canActivate(): it propagates unchanged to the exception filters. ThrowUnauthorizedExceptionto answer401, or any otherHttpExceptionto choose the status yourself.
A guard may be synchronous, return a Promise (an async guard that calls a database or a session store) or return an
Observable. Guards run after all middleware and before any interceptor or pipe, which makes them the right place for
decisions that need to know which handler is about to run. See Request Lifecycle
for the complete order.
ExecutionContext
canActivate() receives an ExecutionContext. It extends ArgumentsHost (the same object that exception filters
receive, see Exception Filters & Error Handling) and adds two methods:
-
getHandler()returns a reference to the method that will be invoked next (forPOST /books, thecreatemethod). -
getClass()returns the controller class (not an instance) that the handler belongs to.
From ArgumentsHost it inherits getType() and the switchToHttp(), switchToWs() and switchToRpc() helpers. In
an HTTP application switchToHttp().getRequest() returns the platform request (Express Request or Fastify
FastifyRequest); pass the type as a type argument to get typed access:
const request = context.switchToHttp().getRequest<Request>();
const handlerName = context.getHandler().name; // "create"
const className = context.getClass().name; // "BooksController"
getHandler() and getClass() are what make guards more powerful than middleware: together with Reflector they let a
guard read metadata attached to the exact handler and controller about to run (see Role-based guards with Reflector).
One guard, several application types
getType() returns 'http', 'rpc' or 'ws', and packages such as @nestjs/graphql add their own value ('graphql').
A guard that is reused across transports should branch on it, because switchToHttp().getRequest() is meaningless in a
microservice handler:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class HttpOnlyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
if (context.getType() !== 'http') {
return true; // let other transports authorize in their own way
}
const request = context.switchToHttp().getRequest();
return Boolean(request.headers['x-client']);
}
}
Under GraphQL, the ExecutionContext must first be converted with GqlExecutionContext.create(context) to reach the
resolver arguments; see GraphQL: Other features (execution context). The WebSocket
and microservice variants are described in WebSockets: Guards and
Microservices: Guards.
An authentication guard
Authentication (who is calling?) fits a guard when it needs to know whether the route is protected at all. The guard
below reads a bearer credential, looks it up and attaches the user to the request, so that later guards, pipes and
handlers can read request.user. The lookup service gets its data from ConfigService; in production this is where a
JWT is verified or a session store is queried (see the authentication and authorization pages of this section for the
real mechanisms).
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Role } from './roles.decorator.js';
export interface SessionUser {
id: string;
roles: Role[];
}
@Injectable()
export class SessionsService {
private readonly sessions: Record<string, SessionUser>;
constructor(config: ConfigService) {
// In a real application this is a JWT verification or a session-store lookup.
this.sessions = config.getOrThrow<Record<string, SessionUser>>('sessions');
}
find(sessionId: string): SessionUser | undefined {
return this.sessions[sessionId];
}
}
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import type { Request } from 'express';
import { Public } from './public.decorator.js';
import { SessionsService, SessionUser } from './sessions.service.js';
export type AuthenticatedRequest = Request & { user?: SessionUser };
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly sessions: SessionsService,
) {}
canActivate(context: ExecutionContext): boolean {
const isPublic = this.reflector.getAllAndOverride(Public, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
const [type, credential] = request.headers.authorization?.split(' ') ?? [];
const user = type === 'Bearer' && credential ? this.sessions.find(credential) : undefined;
if (!user) {
throw new UnauthorizedException();
}
request.user = user;
return true;
}
}
A missing or unknown credential produces a 401 with the standard body, because the guard throws
UnauthorizedException:
{"message":"Unauthorized","statusCode":401}
Role-based guards with Reflector
Authorization (what may this caller do?) needs to know what each route requires. Nest lets you attach custom metadata
to handlers and controllers, and Reflector (exported by @nestjs/core and injectable anywhere) reads it back. There are
two ways to create the decorator.
Reflector.createDecorator() (preferred)
Reflector.createDecorator<T>() returns a strongly typed decorator, and the same decorator object is the key used to
read the value, so there is no string key to mistype and reflector.get() returns T without a cast:
import { Reflector } from '@nestjs/core';
export enum Role {
Reader = 'reader',
Editor = 'editor',
Admin = 'admin',
}
export const Roles = Reflector.createDecorator<Role[]>();
The guard reads the required roles with getAllAndOverride(), so a role set on a method wins over one set on the
controller, and lets the request through when no @Roles() is present anywhere:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import type { AuthenticatedRequest } from './auth.guard.js';
import { Roles } from './roles.decorator.js';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride(Roles, [
context.getHandler(),
context.getClass(),
]);
if (!required) {
return true; // no @Roles() anywhere: any authenticated caller may pass
}
const { user } = context.switchToHttp().getRequest<AuthenticatedRequest>();
return !!user && required.some((role) => user.roles.includes(role));
}
}
Applying it is a matter of decorating handlers. Roles takes a single Role[] argument, exactly as typed:
import { Controller, Delete, Get, Param, Post, Req, UseGuards } from '@nestjs/common';
import type { AuthenticatedRequest } from './auth.guard.js';
import { HeaderGuard, ReviewsEnabledGuard } from './feature-flag.guard.js';
import { Permissions } from './permissions.decorator.js';
import { PermissionsGuard } from './permissions.guard.js';
import { Public } from './public.decorator.js';
import { Role, Roles } from './roles.decorator.js';
@Controller('books')
export class BooksController {
@Public()
@Get()
list() {
return [{ id: 1, title: 'Dune' }];
}
@Get('me')
whoAmI(@Req() request: AuthenticatedRequest) {
return request.user;
}
@Post()
@Roles([Role.Editor, Role.Admin])
create() {
return { id: 2, title: 'Emma' };
}
@Delete(':id')
@Roles([Role.Admin])
remove(@Param('id') id: string) {
return { deleted: id };
}
}
When a reader tries to create a book, the guard returns false and the framework answers with the 403 body that Nest
generates for a rejected guard:
curl -i -X POST http://localhost:3000/books -H "Authorization: Bearer $SESSION_ID"
{"message":"Forbidden resource","error":"Forbidden","statusCode":403}
getAllAndOverride() versus getAllAndMerge()
When the same decorator appears on both the controller and the handler, you choose how the two levels combine:
| Method | Result with @Roles(['user']) on the class and @Roles(['admin']) on the handler |
|---|---|
|
Reads one target only: |
|
The first defined value in the order of the targets: |
|
Arrays are concatenated and objects merged, in target order: |
SetMetadata() (the low-level approach)
@SetMetadata(key, value) from @nestjs/common does the same with an explicit string key. It remains useful when you
need to control the metadata key (for example to interoperate with a library that reads it) or to build a decorator with
rest parameters. Wrap it in your own decorator instead of using it directly on routes, and read it with the same key:
import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions';
export const Permissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PERMISSIONS_KEY } from './permissions.decorator.js';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// getAllAndMerge concatenates class-level and handler-level values.
const needed = this.reflector.getAllAndMerge<string[]>(PERMISSIONS_KEY, [
context.getHandler(),
context.getClass(),
]);
const request = context.switchToHttp().getRequest<{ headers: Record<string, string> }>();
const granted = (request.headers['x-granted'] ?? '').split(',');
return needed.every((p) => granted.includes(p));
}
}
@Controller('exports')
@Permissions('export')
@UseGuards(PermissionsGuard)
export class ExportsController {
@Get()
csv() { return 'id,title'; } // needs: export
@Get('all')
@Permissions('export-all')
all() { return 'everything'; } // needs: export and export-all
}
The wrapped decorators are covered in depth in Custom Decorators, including
composing several of them with applyDecorators().
Binding guards: @UseGuards(), useGlobalGuards() and APP_GUARD
Like pipes, interceptors and exception filters, a guard can be bound at three scopes.
| Scope | How | Dependency injection |
|---|---|---|
Route |
|
Yes when you pass the class; Nest instantiates it. |
Controller |
|
Yes when you pass the class. |
Global, outside a module |
|
No: registration happens outside any module, so the instance is built by hand. |
Global, inside a module |
a provider |
Yes: the guard is a normal provider of that module and can inject any provider visible to it. |
Source: NestJS: Guards - Binding guards
@UseGuards() accepts one guard or a comma-separated list, and guards run left to right. Passing the class leaves
instantiation (and injection) to Nest; passing an instance (new HeaderGuard('x-internal')) works but nothing is
injected, so the guard must get what it needs from its constructor. Both forms appear below: ReviewsEnabledGuard
injects ConfigService and returns an Observable<boolean>, while HeaderGuard is built in place and rejects with 404
to hide an internal route.
import { CanActivate, Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Observable, of } from 'rxjs';
// Resolved by Nest (class form): the ConfigService is injected.
@Injectable()
export class ReviewsEnabledGuard implements CanActivate {
constructor(private readonly config: ConfigService) {}
canActivate(): Observable<boolean> {
// canActivate may return boolean | Promise<boolean> | Observable<boolean>
return of(this.config.get<boolean>('features.reviews') ?? false);
}
}
// Instance form: nothing is injected, so the guard gets its inputs from the constructor.
export class HeaderGuard implements CanActivate {
constructor(private readonly header: string) {}
canActivate(context: import('@nestjs/common').ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<{ headers: Record<string, string> }>();
if (!request.headers[this.header]) {
throw new NotFoundException();
}
return true;
}
}
@Controller('reviews')
@UseGuards(ReviewsEnabledGuard)
export class ReviewsController {
@Get()
list() {
return [];
}
@Get('internal')
@UseGuards(new HeaderGuard('x-internal'))
internal() {
return { ok: true };
}
}
Source: NestJS: Guards - Binding guards
Global guards with APP_GUARD
Use APP_GUARD whenever the global guard needs injection, which is nearly always (Reflector itself is injected). The
guard is global regardless of the module that declares the provider; declare it next to the guard’s own module for clarity.
Registering the token several times is allowed, and every registered guard runs on every request, in registration order:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth.guard.js';
import { BooksController, ExportsController, ReviewsController } from './books.controller.js';
import { Role } from './roles.decorator.js';
import { RolesGuard } from './roles.guard.js';
import { SessionsService } from './sessions.service.js';
@Module({
imports: [
ConfigModule.forRoot({
ignoreEnvFile: true,
load: [
() => ({
// Fixture data for the example; production code verifies a signed token instead.
sessions: {
'reader-session': { id: 'u1', roles: [Role.Reader] },
'editor-session': { id: 'u2', roles: [Role.Editor] },
'admin-session': { id: 'u3', roles: [Role.Admin] },
},
features: { reviews: true },
}),
],
}),
],
controllers: [BooksController, ReviewsController, ExportsController],
providers: [
SessionsService,
// Registration order is execution order: authenticate first, then authorize.
{ provide: APP_GUARD, useClass: AuthGuard },
{ provide: APP_GUARD, useClass: RolesGuard },
],
})
export class GuardsModule {}
Source: NestJS: Guards - Binding guards
Two details about APP_GUARD are easy to miss. It is a pseudo-provider consumed during bootstrap, so it cannot be
fetched afterwards with app.get() or injected into another provider. And app.useGlobalGuards() guards always run
after every APP_GUARD, whatever the registration order, which is why an authentication guard that must run first
belongs in APP_GUARD form (see Security: Authorization).
In a hybrid application (an HTTP server with connected microservices), useGlobalGuards() does not apply to the
connected microservices by default; connect them with inheritAppConfig: true and call the useGlobal*() methods before
connectMicroservice(). See FAQ: Hybrid application.
A global guard with a @Public() opt-out
The safest default for an API is "everything requires authentication unless declared otherwise", because a forgotten
decorator then fails closed. A global guard plus an opt-out decorator implements it. The Public decorator in the
example uses the transform option of Reflector.createDecorator() so that @Public() takes no argument at the call
site yet stores true:
import { Reflector } from '@nestjs/core';
// No argument at the call site (`@Public()`); the stored value is `true`.
export const Public = Reflector.createDecorator<void, boolean>({
transform: () => true,
});
AuthGuard above already honours it: it reads Public with getAllAndOverride() over the handler and the class, so
@Public() works on a single route (GET /books) or on a whole controller. The resulting behaviour:
curl -i http://localhost:3000/books # 200, no credentials needed
curl -i http://localhost:3000/books/me # 401, guard threw UnauthorizedException
curl -i http://localhost:3000/books/me -H "Authorization: Bearer $SESSION_ID" # 200
{"id":"u1","roles":["reader"]}
Handlers without @Roles() are reachable by any authenticated user, so a global role guard needs the same discipline as
any default-allow rule: review new routes, and prefer an explicit @Roles() on every non-public handler. The official
@nestjs/authentication and @nestjs/authorization packages (both pre-1.0 at the time of writing) ship ready-made
@Public() and @Can() decorators on top of this exact mechanism; they are described on the security pages.
Order of execution
Guards run in this order: global guards first (APP_GUARD providers in registration order, then useGlobalGuards()),
then controller guards, then route guards; inside each level they run in the order they are listed. A guard that returns
false or throws stops the chain, so later guards and everything after them (interceptors, pipes, the handler) never run.
Three guards that record their names produce this order:
["global-1","global-2","controller","route"]
Because AuthGuard is registered before RolesGuard, request.user is populated by the time the roles check reads it.
Swap the two and every non-public request sees an anonymous caller.
Guards versus middleware
Both can reject a request before the handler runs, so the distinction is about what each knows:
| Middleware | Guard | |
|---|---|---|
Position |
Before all guards, interceptors and pipes |
After middleware, before interceptors and pipes |
Knows the target handler |
No: after |
Yes: |
Platform |
Receives the platform |
Platform-independent: HTTP, GraphQL, WebSockets, microservices through the same interface |
Typical use |
Cross-cutting request preparation: logging, correlation ids, CORS, body or cookie parsing, attaching a token’s claims |
Allow or deny based on route metadata: roles, permissions, feature flags, API-key scopes |
Dependency injection |
Class middleware yes, functional middleware no |
Yes, with the class forms of binding |
Rejecting |
Write the response or call |
Return |
Source: NestJS: Guards and NestJS: Middleware
Rule of thumb: parsing and decorating the request belongs in Middleware; deciding
whether the matched route may run belongs in a guard. Validating and transforming the input of an allowed request is the
job of Pipes & Validation, and wrapping the handler with cross-cutting logic
is the job of Interceptors. The authentication example above is deliberately a
guard (it needs the @Public() metadata); a pure "attach the user if a token is present" step that never rejects could
equally be middleware.
Express and Fastify
Guards are platform-agnostic; only the request object you read from differs. switchToHttp().getRequest() returns an
Express Request or a Fastify FastifyRequest, and in both cases header names are lower-cased
(request.headers.authorization). The same module behaves identically on both adapters, with the same results for
public (200), anonymous (401) and insufficient-role (403) requests. Keep platform-specific types out of reusable
guards where you can: read what you need through a small helper, or type the request as a minimal structural type as the
HeaderGuard above does.
Testing a guard
A guard is a plain class, so a unit test can call canActivate() with a hand-built context that provides only what the
guard reads, and an end-to-end test (Test.createTestingModule() plus supertest) verifies the whole chain including
the global registration:
const guard = new RolesGuard(new Reflector());
const context = {
getHandler: () => () => undefined,
getClass: () => class {},
switchToHttp: () => ({ getRequest: () => ({}) }),
} as unknown as ExecutionContext;
expect(guard.canActivate(context)).toBe(true); // no @Roles() on the handler or the class
The testing page of this section covers overrideGuard(), overrideProvider() and the end-to-end setup in detail.
Pitfalls
-
Guards run before pipes. A guard sees the raw, unvalidated request, so never trust
request.bodythere; authorize on identity and metadata, and validate input in pipes. -
Default-allow. A role guard that returns
truewhen no@Roles()is present protects nothing by itself. Combine it with the authentication guard and review routes without the decorator. -
Instance-bound and
useGlobalGuards()guards get no injection. Use the class form orAPP_GUARD. -
Global order.
APP_GUARDguards run in registration order, thenuseGlobalGuards()guards. Authenticate before you authorize. -
Throwing versus returning
false. Returningfalsealways yields403. If an unauthenticated caller must get401, throwUnauthorizedException. -
Request-scoped dependencies. Injecting a request-scoped provider into a guard makes the guard request-scoped, with a per-request instantiation cost (see Providers & Dependency Injection).
-
Field resolvers in GraphQL. By default guards run only for the top-level query or mutation, not for each
@ResolveField(); thefieldResolverEnhancersoption ofGqlModuleOptionsopts in.
Where the book is outdated
The reference book, published in January 2025 (see the section bibliography),
introduces role checks with @SetMetadata('roles', …) and a Reflector.get<string[]>('roles', …) call. That still
works, but the current documentation prefers Reflector.createDecorator<T>() (typed, no string key) and shows
getAllAndOverride()/getAllAndMerge() for class-plus-handler metadata. Its examples also predate Nest 12, so they use
CommonJS-style imports; in the ESM template, relative imports carry a .js extension, as in the examples above. The
guard concept itself (CanActivate, ExecutionContext, @UseGuards, APP_GUARD) is unchanged.
What changed recently
-
The documentation now recommends
Reflector.createDecorator()as the primary way to define route metadata, with@SetMetadata()presented as the low-level alternative. -
The 2026 documentation restructure moved the authentication and authorization material into dedicated Security chapters built around
@nestjs/authenticationand@nestjs/authorization, both pre-1.0 (0.0.x) packages that register global guards of their own; the guard mechanism described here is what they build on. -
The docs now state explicitly that
APP_GUARDcan be registered several times, that every registered guard runs in registration order, and thatAPP_*tokens cannot be retrieved withapp.get(). -
For hybrid applications,
inheritAppConfig: truemakesconnectMicroservice()reuse the application’s global guards.