Discovery service

A way to append decorators during system startup.

This improvement will be an opportunity to learn about a very interesting and useful tool. We could make that all non-public routes receive the @ApiUnauthorizedResponse() decorator, which indicates that a route may return a response with the UNAUTHORIZED status. The same could be thought about the routes that require roles, and the @ApiForbiddenResponse() decorator. To achieve this, we can leverage the DiscoveryService, which allows for accessing references to all controllers and providers during system startup, which is quite useful for appending decorators dynamically, for instance. Let's then begin.

First, let's create a module to encapsule these documentation mappers.

nest g mo docs

Then, let's create a class for the Unauthorized mapper.

nest g cl docs/docs-unauthorized-mapper

Please perform the following:

  • Alter the filename to end in .mapper

  • Add @Injectable(), so that it can be accessed by the Nest IoC container

Now, in the module, we should add this mapper to the providers, and the DiscoveryModule to the imports, in order to use the DiscoveryService.

The next step is to, back in the class, make it implement the OnApplicationBootstrap interface. This forces the class to implement the namesake method, which executes when the app is starting. Now, the first step in this method will be to obtain a reference to the controllers by using the DiscoveryService, which we also should inject now.

const controllers = this.discoveryService.getControllers();

We'll then iterate over each controller. Notice that they were returned inside wrappers.

controllers.forEach((wrapper) => {
  // ...
});

Next, we'll obtain the instance from inside each wrapper, and then obtain the prototype from the instance. This will be necessary afterwards.

const { instance } = wrapper;
const prototype = Object.getPrototypeOf(instance);

Some parts now will be a bit familiar. We should then use a reflector to get the isPublic metadata from the controller itself. Here we have no access to the context like we did inside the guards, so we should pass instance.constructor. If the controller is public, simply return.

const isControllerPublic = this.reflector.get<boolean>(
  IS_PUBLIC_KEY,
  instance.constructor,
);
if (isControllerPublic) return;

After that, we'll get the names of the controller's route handlers by using the metadataScanner (also to be injected). This can be achieved by using the prototype. Sequentially, we may obtain references to the actual route handlers by indexing these names inside the controller instance.

const routeNames = this.metadataScanner.getAllMethodNames(prototype);
const routes = routeNames.map((name) => instance[name]);

Nearing the end, we'll now iterate over each route.

routes.forEach((route) => {
  // ...
});

The same process will now be performed with each handler: if it's public, simply return.

const isPublic = this.reflector.get<boolean>(IS_PUBLIC_KEY, route);
if (isPublic) return;

Finally, to append this decorator to the route, use it without @, and use a second pair of parentheses, passing the route.

ApiUnauthorizedResponse({ description: 'Unauthorized' })(route);

And we're done! Now these routes will be automatically decorated for us.

Commit - Using discovery service to automatically document routes

Let's now create the Forbidden mapper. Please repeat the aforementioned procedure, and I'll detail then what will be different.

In this case, we'll check if the controller is protected, that is, if it requires roles.

const isControllerProtected = !!this.reflector.get<Role[]>(
  ROLES_KEY,
  instance.constructor,
);

If it is, all its routes will be marked with the decorator and this iteration ends.

if (isControllerProtected) {
  routes.forEach((route) => {
    ApiForbiddenResponse({ description: 'Forbidden' })(route);
  });
  return;
}

If not, we'll then check each handler, and it won't be marked if it's not protected.

const isProtected = !!this.reflector.get<Role[]>(ROLES_KEY, route);
if (!isProtected) return;

We have now covered both cases with dynamic documentation. Outstanding!

Commit - Documenting forbidden routes automatically

Last updated