Execution ContextFundamentals · NestJS

Put metadata on a class as the default and on handlers as the override, mark handlers public with SetMetadata, and read it all in a global guard with getAllAndOverride.

What you will learn

Read the theory for Execution Context

All Fundamentals lessons

All NestJS courses

loading types…

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.

  1. Write Public() in public.decorator.ts: it stores true under IS_PUBLIC_KEY.
  2. 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 a 403.
  3. On CatsController, require the reader role for every handler by default, admin for creating and deleting, and nothing at all for GET /cats/health.

StatsController has no metadata anywhere and should stay open.

When it fails

  • /cats/health answers 401 or 403: the guard reads roles before checking IS_PUBLIC_KEY, or reads the class before the handler and finds reader. Public first, handler before class.
  • GET /cats is open to anonymous requests: the guard uses reflector.get(Roles, context.getHandler()), which never sees the class. Use getAllAndOverride with both targets.
  • POST /cats as ada answers 403: getAllAndMerge produced ['admin', 'reader'] and the check demands every role, or the targets are in class-then-handler order so the class's reader overrode the handler's admin.
  • /stats answers 401: the guard treats undefined roles as "everything forbidden". No metadata means open.
  • Nest can't resolve dependencies of the RolesGuard (?): the guard was registered with useGlobalGuards(new RolesGuard()), which cannot inject the Reflector. APP_GUARD can.

Remember

  • ArgumentsHost holds the handler's arguments for any transport; switchToHttp() and getType() keep code transport-safe.
  • ExecutionContext adds getHandler() and getClass(), the two targets metadata lives on.
  • Reflector.createDecorator and SetMetadata both attach metadata; the Reflector reads it by decorator or by key.
  • getAllAndOverride([handler, class]) is default-and-override; getAllAndMerge accumulates. APP_GUARD makes 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.