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. We begin by finishing the business of the previous lesson: how middleware attaches itself to the application at all.
A middleware class does nothing on its own until you say where it should run. That is the job of a module implementing the
NestModule interface and filling in its configure method:1import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
2
3@Module({
4 controllers: [TributesController],
5})
6export class AppModule implements NestModule {
7 configure(consumer: MiddlewareConsumer) {
8 consumer.apply(LoggerMiddleware).forRoutes('tributes');
9 }
10}The written order is always the same:
opens the method, configure(consumer: MiddlewareConsumer) {
names the middleware class, consumer.apply(LoggerMiddleware)
names the routes it should run on, and the brace .forRoutes('tributes')
closes the whole. The process has four steps: create the middleware class with }
@Injectable(), implement NestModule in the module class, call consumer.apply() inside configure(), and finally specify the routes with .forRoutes().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.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.
A guard is a class implementing the
interface. Not CanActivate
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:
takes the context, canActivate(context: ExecutionContext) {
extracts the request object from it, const request = context.switchToHttp().getRequest();
reaches for the header, and const token = request.headers.authorization;
turns it into a decision.return !!token;
That decision is the boolean
when access is to be granted - not the string true
'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.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?".
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:
opens the decorator, @UseGuards(
names the guard class - the class, not an instance of it, because creation is left to dependency injection - and AuthGuard
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.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.The outer post counts those coming in, the treasury sentry decides, @name:
NestModule: configure(consumer: MiddlewareConsumer) { → consumer.apply(LoggerMiddleware) → .forRoutes('tributes') → },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,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,@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".