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 a ForbiddenException, which the exceptions layer turns into a 403 Forbidden response.

  • An exception thrown from canActivate(): it propagates unchanged to the exception filters. Throw UnauthorizedException to answer 401, or any other HttpException to 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 (for POST /books, the create method).

  • 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).

The session lookup used by the examples (fixture data comes from ConfigService)
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];
  }
}
A global authentication guard that skips routes marked @Public()
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:

A typed @Roles() decorator
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:

A roles guard
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:

Controller with public, authenticated and role-restricted routes
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

get(decorator, target)

Reads one target only: context.getHandler() or context.getClass().

getAllAndOverride(decorator, [handler, class])

The first defined value in the order of the targets: ['admin']. Use it for "class default, handler override".

getAllAndMerge(decorator, [handler, class])

Arrays are concatenated and objects merged, in target order: ['admin', 'user']. Use it when requirements accumulate.

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:

A permissions decorator built on SetMetadata and a guard that merges both levels
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

@UseGuards(Guard) on a handler method

Yes when you pass the class; Nest instantiates it.

Controller

@UseGuards(Guard) on the controller class

Yes when you pass the class.

Global, outside a module

app.useGlobalGuards(new Guard()) in main.ts

No: registration happens outside any module, so the instance is built by hand.

Global, inside a module

a provider { provide: APP_GUARD, useClass: Guard }

Yes: the guard is a normal provider of that module and can inject any provider visible to it.

@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.

A class-bound guard with injection (returning an Observable) and an instance-bound guard
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 };
  }
}

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:

Registering the authentication and roles guards globally (order matters)
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 {}

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:

The @Public() decorator
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 next() it does not know what runs next

Yes: ExecutionContext exposes getHandler() and getClass() and therefore their metadata

Platform

Receives the platform req, res, next (Express or Fastify differences apply)

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 next(error) yourself

Return false or throw an exception; exception filters shape the response

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.body there; authorize on identity and metadata, and validate input in pipes.

  • Default-allow. A role guard that returns true when 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 or APP_GUARD.

  • Global order. APP_GUARD guards run in registration order, then useGlobalGuards() guards. Authenticate before you authorize.

  • Throwing versus returning false. Returning false always yields 403. If an unauthenticated caller must get 401, throw UnauthorizedException.

  • 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(); the fieldResolverEnhancers option of GqlModuleOptions opts 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/authentication and @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_GUARD can be registered several times, that every registered guard runs in registration order, and that APP_* tokens cannot be retrieved with app.get().

  • For hybrid applications, inheritAppConfig: true makes connectMicroservice() reuse the application’s global guards.