Files
What you'll learn
- Tell ArgumentsHost from ExecutionContext, and know what getType(), getArgs(), getHandler() and getClass() give
- Write a metadata decorator with SetMetadata and read it with the Reflector by key, next to one made with Reflector.createDecorator
- Merge class-level and handler-level metadata with getAllAndOverride, and register a global guard under APP_GUARD
Execution Context
Guards, interceptors, filters and custom decorators all receive an object describing what is about to run, and Basics used it without looking closely. This lesson looks closely, because it is the key to writing enhancers that work anywhere in an application, whatever the transport, and to attaching rules to controllers and handlers with metadata instead of code. The exercise puts a global guard in front of every route, with defaults on a class, overrides on handlers, and one escape hatch.
ArgumentsHost
ArgumentsHost wraps the arguments a handler was called with, whatever kind of handler it is. For an HTTP route the arguments are Express's [request, response, next]; for a WebSocket gateway they are [client, data]; for a GraphQL resolver [root, args, context, info]. Reaching in by position works, host.getArgByIndex(0) is the request, but the position depends on the transport. The typed accessors do not:
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
switchToRpc() and switchToWs() are the others, and host.getType() says which applies: 'http', 'rpc' or 'ws' (or 'graphql' when @nestjs/graphql is in play). An exception filter meant for every transport branches on it; one meant for HTTP only switches straight away, as the filter in Basics did.
ExecutionContext
ExecutionContext extends ArgumentsHost with two methods that only make sense once the router has chosen a handler:
const handler = context.getHandler(); // the method about to run, e.g. findAll
const controller = context.getClass(); // its class, e.g. CatsController
Guards and interceptors receive it; filters and parameter decorators receive the plain host, because for a filter no handler may have been chosen (a 404 has none). getHandler().name and getClass().name are what the logging interceptor in Basics printed. Their real use is as keys: metadata is stored on the handler function and on the class, and these two methods are how a guard gets at both.
Two ways to attach metadata
Reflector.createDecorator<T>() from Basics makes a typed decorator and its own reflection key in one line:
export const Roles = Reflector.createDecorator<string[]>();
The older, lower-level form is SetMetadata(key, value) from @nestjs/common, wrapped in a small function so call sites read well:
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Both put a value on the target and both are read through the Reflector; the difference is the key. reflector.get(Roles, target) takes the decorator itself, and knows the value is a string[]. reflector.get<boolean>(IS_PUBLIC_KEY, target) takes the string key, and the type is whatever you say. Prefer createDecorator for your own metadata; know SetMetadata because most of the ecosystem, and the docs' authentication chapter, use it.
Class and handler together
A decorator can go on a controller class as well as on a handler. @Roles(['reader']) on the class means every handler in it requires a reader, without repeating the line. Handlers then need a way to say something different, and the guard needs a way to combine the two readings. The Reflector has two:
const roles = this.reflector.getAllAndOverride(Roles, [context.getHandler(), context.getClass()]);
const roles = this.reflector.getAllAndMerge(Roles, [context.getHandler(), context.getClass()]);
getAllAndOverride returns the first value it finds in the order given: the handler's when it has one, otherwise the class's, otherwise undefined. That is the semantics of a default and an override. getAllAndMerge concatenates arrays and merges objects from every target, for metadata that accumulates rather than replaces. Put the handler first in the list for both, so the more specific target wins.
A guard for the whole application
@UseGuards() on every controller is easy to forget on the next one. A global guard covers everything, and because it needs the Reflector, it is registered as a provider under the APP_GUARD token rather than with app.useGlobalGuards(), which cannot inject:
@Module({
providers: [{ provide: APP_GUARD, useClass: RolesGuard }],
})
export class AppModule {}
Once every route goes through the guard, routes that should be open need to say so, which is what @Public() is for: the guard checks it first and lets the request through before looking at roles at all. APP_PIPE, APP_INTERCEPTOR and APP_FILTER register the other enhancers the same way.
Your task
RolesGuard is already global, through APP_GUARD, and CurrentUserMiddleware attaches ada (admin) and bob (reader) from the x-user header as in Basics.
- Write
Public()inpublic.decorator.ts: it storestrueunderIS_PUBLIC_KEY. - Complete the guard. A handler or class marked public is open, whatever else says. Otherwise the roles required are the handler's, or the class's when the handler has none, and none anywhere means open. Required roles and no user is a
401; a user without any of them is a403. - On
CatsController, require the reader role for every handler by default, admin for creating and deleting, and nothing at all forGET /cats/health.
StatsController has no metadata anywhere and should stay open.
When it fails
/cats/healthanswers 401 or 403: the guard reads roles before checkingIS_PUBLIC_KEY, or reads the class before the handler and findsreader. Public first, handler before class.GET /catsis open to anonymous requests: the guard usesreflector.get(Roles, context.getHandler()), which never sees the class. UsegetAllAndOverridewith both targets.POST /catsasadaanswers 403:getAllAndMergeproduced['admin', 'reader']and the check demands every role, or the targets are in class-then-handler order so the class'sreaderoverrode the handler'sadmin./statsanswers 401: the guard treatsundefinedroles as "everything forbidden". No metadata means open.Nest can't resolve dependencies of the RolesGuard (?): the guard was registered withuseGlobalGuards(new RolesGuard()), which cannot inject theReflector.APP_GUARDcan.
Remember
ArgumentsHostholds the handler's arguments for any transport;switchToHttp()andgetType()keep code transport-safe.ExecutionContextaddsgetHandler()andgetClass(), the two targets metadata lives on.Reflector.createDecoratorandSetMetadataboth attach metadata; theReflectorreads it by decorator or by key.getAllAndOverride([handler, class])is default-and-override;getAllAndMergeaccumulates.APP_GUARDmakes a guard global with injection.
Stuck? Show a hint
Public: () => SetMetadata(IS_PUBLIC_KEY, true). In the guard, targets = [context.getHandler(), context.getClass()]; reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, targets) first, then reflector.getAllAndOverride(Roles, targets) for the roles; the rest is the guard from Basics. On the controller: @Roles(['reader']) on the class, @Roles(['admin']) on create and remove, @Public() on health.
Press Run tests to start the app. Its log appears here.Graded endpoints
The class requires reader, the handler says public, and the handler wins: getAllAndOverride read the handler first
Neither the handler nor the class requires anything, so the global guard lets it through
The handler has no roles of its own, so the class's reader applies, and there is no user
bob is a reader, which the class requires
The handler overrides the class with admin; bob is not one
An admin
Class default again: reader
Overridden to admin
An admin may