NestJS course Β· Module 2: Routing and Request Lifecycle

Guards - who gets through the gate

6 min read
In this lesson6

The sentry at the outer post counts everyone coming in and writes down the hour. He does the same for a merchant, a courier and a legionary - because at the outer post it is not yet known where any of them is headed. Only at the treasury door stands someone who knows which chamber the visitor is knocking at, and who can therefore say "not you".

That is the whole difference between middleware and a guard, and in this lesson we shall close it out.

In the previous lesson we attached middleware in a module through configure(consumer) and .forRoutes('tributes'). Note the argument 'tributes': middleware is given a path pattern, not the name of a controller or a method - and that is the first clue to what it lacks.

What middleware does not know

Middleware executes before the guard and does not know the target route. This is no oversight on the framework authors' part but a consequence of timing: middleware runs at the Express level, before NestJS has settled which controller and which method will handle the request. It receives req, res and next - three HTTP objects and nothing beyond them.

That makes middleware excellent for anything independent of the destination: logging, CORS headers, cookie parsing. It is unfit for decisions of the kind "this endpoint requires an administrator role", because at the moment it runs, the notion of "this endpoint" does not yet exist.

CanActivate - the sentry's interface

A guard is a class implementing the CanActivate interface. Not NestMiddleware - that belongs to middleware from the previous lesson; not NestInterceptor - those are interceptors, which come next; not PipeTransform - those are pipes, further on still. Each of the four mechanisms of the request cycle has its own interface and its own single method:

1@Injectable()
2export class AuthGuard implements CanActivate {
3  canActivate(context: ExecutionContext): boolean {
4    const request = context.switchToHttp().getRequest();
5    const token = request.headers.authorization;
6
7    return !!token;
8  }
9}

The body of the method reads in four steps, always in this order: canActivate(context: ExecutionContext) { takes the context, const request = context.switchToHttp().getRequest(); extracts the request object from it, const token = request.headers.authorization; reaches for the header, and return !!token; turns it into a decision.

That decision is the boolean true when access is to be granted - not the string 'allowed', not an object with roles, not null. The double exclamation mark in !!token does exactly that: it turns "the header is there or it is not" into true or false. When false comes back, NestJS rejects the request with 403 Forbidden, and the controller method does not run at all.

ExecutionContext - the missing piece

The context argument is why a guard can do more than middleware. ExecutionContext provides information about the target controller and method (handler) - not database access, not file system access, not server configuration. Those are obtained elsewhere: the database by injecting a repository, the configuration through ConfigService.

Two methods will do for a start. context.switchToHttp().getRequest() descends to the HTTP layer for the familiar request object - "switch", because NestJS also serves WebSockets and microservices, and the context is common to all of them. context.getHandler() returns the controller method itself that is to handle the request, and context.getClass() returns the controller class.

And this is precisely what middleware lacks. A guard, holding the handler, can ask: "what does this particular method require?".

Attaching a guard

A guard is attached with the @UseGuards decorator - on a single method or on a whole controller:

1@Controller('treasury')
2@UseGuards(AuthGuard)
3export class TreasuryController {
4  @Get()
5  findAll() {
6    return this.treasuryService.findAll();
7  }
8}

The notation has three parts: @UseGuards( opens the decorator, AuthGuard names the guard class - the class, not an instance of it, because creation is left to dependency injection - and ) closes it. Several guards may be listed, separated by commas; they are checked in turn, and a single false is enough for the request to fall.

When a sentry is to watch the whole application, you register it globally: app.useGlobalGuards(new AuthGuard()) in main.ts, or as a provider with the APP_GUARD token. The second road is the better one when the guard needs something itself - a provider goes through dependency injection, new does not.

Reflector - a guard that reads requirements

Since a guard knows the handler, it can read the requirements written beside it. Requirements are pinned on with SetMetadata and read back with the Reflector class:

1export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
2
3@Injectable()
4export class RolesGuard implements CanActivate {
5  constructor(private reflector: Reflector) {}
6
7  canActivate(context: ExecutionContext): boolean {
8    const requiredRoles = this.reflector.get<string[]>(
9      'roles',
10      context.getHandler(),
11    );
12
13    if (!requiredRoles) {
14      return true;
15    }
16
17    const request = context.switchToHttp().getRequest();
18
19    return requiredRoles.includes(request.user?.role);
20  }
21}

SetMetadata('roles', roles) sticks a label on the method under the key 'roles'. this.reflector.get<string[]>('roles', context.getHandler()) reads it back - the first argument is the key, the second is where to look for it, namely our handler. No label means an endpoint with no requirements, so we return true and let it through.

One guard serves the whole application this way: @Roles('senator') above one method, @Roles('centurion', 'tribune') above another, and the comparison logic written once. It is the same SetMetadata we shall return to with custom decorators - there you will see how to roll @Roles and @UseGuards into a single seal.

Summary

The outer post counts those coming in, the treasury sentry decides:

  • middleware is registered in a module implementing NestModule: configure(consumer: MiddlewareConsumer) { β†’ consumer.apply(LoggerMiddleware) β†’ .forRoutes('tributes') β†’ },
  • middleware executes before the guard and does not know the target route - it runs before NestJS has settled which handler will serve the request,
  • a guard implements the CanActivate interface - not NestMiddleware, not NestInterceptor, not PipeTransform,
  • canActivate() returns true to allow access - not the string 'allowed', not an object with roles, not null; false gives a 403 and the handler does not run,
  • the order within the method: canActivate(context: ExecutionContext) { β†’ const request = context.switchToHttp().getRequest(); β†’ const token = request.headers.authorization; β†’ return !!token;,
  • ExecutionContext provides information about the target controller and method (handler) - not the database, not the file system, not server configuration,
  • context.getHandler() returns the method, context.getClass() the controller, switchToHttp().getRequest() the request object,
  • attaching: @UseGuards( β†’ AuthGuard β†’ ), globally through useGlobalGuards or the APP_GUARD token,
  • SetMetadata('roles', roles) stores requirements beside the handler, and Reflector reads them: this.reflector.get<string[]>('roles', context.getHandler()).

In the next lesson you will meet interceptors - the third mechanism of the cycle, and the first to touch the response rather than only the request. For now remember the difference: middleware asks "what has arrived", a guard asks "where is it going, and is that allowed".

Code for this lesson: src/guards.ts
1// Guards in NestJS - Elite Treasury Guards
2import {
3  Injectable, CanActivate, ExecutionContext,
4  SetMetadata, UnauthorizedException, ForbiddenException,
5} from '@nestjs/common';
6import { Reflector } from '@nestjs/core';
7
8console.log("Guards - elite guards deciding on access!");
9
10// ===========================================
11// 1. Simple AuthGuard
12// ===========================================
13
14@Injectable()
15export class RomanAuthGuard implements CanActivate {
16  canActivate(context: ExecutionContext): boolean {
17    const request = context.switchToHttp().getRequest();
18    const authHeader = request.headers['authorization'];
19
20    if (!authHeader || !authHeader.startsWith('Bearer ')) {
21      throw new UnauthorizedException('Missing authorization token!');
22    }
23
24    const token = authHeader.split(' ')[1];
25
26    // In a real application: JWT verification
27    if (token !== 'roman-secret-token') {
28      throw new UnauthorizedException('Invalid token!');
29    }
30
31    // Add user data to the request
32    request.user = { id: 1, name: 'Marcus', roles: ['Centurio'] };
33    return true;
34  }
35}
36
37// ===========================================
38// 2. RolesGuard with metadata
39// ===========================================
40
41export const ROLES_KEY = 'roles';
42export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
43
44@Injectable()
45export class RolesGuard implements CanActivate {
46  constructor(private reflector: Reflector) {}
47
48  canActivate(context: ExecutionContext): boolean {
49    const requiredRoles = this.reflector.get<string[]>(
50      ROLES_KEY,
51      context.getHandler(),
52    );
53
54    // No required roles - allow access
55    if (!requiredRoles || requiredRoles.length === 0) {
56      return true;
57    }
58
59    const request = context.switchToHttp().getRequest();
60    const user = request.user;
61
62    if (!user) {
63      throw new ForbiddenException('User not logged in!');
64    }
65
66    const hasRole = requiredRoles.some(role => user.roles?.includes(role));
67
68    if (!hasRole) {
69      throw new ForbiddenException('Missing required rank: ' + requiredRoles.join(', '));
70    }
71
72    return true;
73  }
74}
75
76// ===========================================
77// 3. Usage in controller
78// ===========================================
79
80// @Controller('senate')
81// @UseGuards(RomanAuthGuard, RolesGuard)
82// export class SenateController {
83//   @Roles('Senator', 'Consul')
84//   @Get('secret-decrees')
85//   getSecretDecrees() {
86//     return { decrees: ['Tajny dekret...'] };
87//   }
88// }
89
90console.log("\n=== GUARDS SUMMARY ===");
91console.log("CanActivate - interface for guards");
92console.log("canActivate() returns true/false");
93console.log("@SetMetadata - setting metadata (e.g. roles)");
94console.log("Reflector - reading metadata");
95console.log("@UseGuards() - applying guard on controller/method");
96

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How does middleware differ from a guard in NestJS?

  2. 2. What interface does a guard implement in NestJS?

  3. 3. What does the canActivate() method of a guard return to allow access to an endpoint?

  4. 4. What does ExecutionContext provide in a guard that middleware does not have?

Hands-on tasks in the game

  • Click in order

    Arrange the middleware configuration elements in the correct order:

  • Code editor

    Implement NestModule in AppModule with a configure() method that registers LoggerMiddleware for 'tributes' routes

  • Code editor

    Write an AuthGuard implementing CanActivate that checks for a token in the request's authorization header

  • Horizontal ordering

    Arrange the elements of using the @UseGuards decorator on a controller:

  • Click in order

    Arrange the elements of the authorization guard implementation in the correct order:

  • Code editor

    Write a RolesGuard that uses Reflector to read roles from SetMetadata and compare them with the user's role

Useful articles