Dan Dean / Journal:

Collecting a list of Nest RouterModule path prefixes

collecting a list of nest routermodule path prefixes module paths

In Nest we declare path components on @Controller(<path>) and method decorators like @Get(<path>), @Post(<path>), etc. But it's also possible to declare a "path prefix" which applies to all controllers in the module via RouterModule:

RouterModule.register({path: '/foo', module: MyModule});

I'm in the middle of building some functionality which needs module router path prefixes, and from a centralized location. Unfortunately, Nest does not provide a first class API for retrieving them. With DiscoveryService it's possible to get path components from @Controller() and method decorators, but not RouterModule path prefixes.

I spent some time digging through Nest source code. There is a GraphInspector class, and that looked promising, but I couldn't find a way to make it useful here. There's also a RouterExplorer class, but it's internal.

In the end I was able to make this work by looking at the metadata Nest stores on modules. I eventually found metadata with the promising name of "__module_path__<bunchofstuff>", and on that is the path prefix!

What follows is my hack of a solution for gather module path prefixes using this bit of knowledge. First, let's declare a minimal module structure to prove out a solution:

import {Controller, Get, Module, OnApplicationBootstrap} from '@nestjs/common';
import {
  ModulesContainer,
  NestFactory,
  Reflector,
  RouterModule,
} from '@nestjs/core';

@Controller('controller-1')
class Controller1 {
  @Get('index')
  index() {}
}

@Module({
  imports: [],
  controllers: [Controller1],
  providers: [],
  exports: [],
})
class Module1 {}

@Controller('controller-2')
class Controller2 {
  @Get('index')
  index() {}
}

@Module({
  imports: [],
  controllers: [Controller2],
  providers: [],
  exports: [],
})
class Module2 {}

The solution will involve:

  • Waiting for the module graph to be completely built
  • Traversal of the module graph
  • Identify each module with __module_path__... metadata
  • Pull out the associated path, filtering down the set of modules to only those with path prefixes
@Module({
  imports: [
    // Here we see each of our modules imported and registered:
    Module1,
    RouterModule.register([{path: 'module-1', module: Module1}]),
    Module2,
    RouterModule.register([{path: 'module-2', module: Module2}]),
  ],
})
export class RouteDiscoveryAppModule implements OnApplicationBootstrap {
  constructor(
    // Inject the global ModuleContainer so we can traverse modules:
    private modulesContainer: ModulesContainer,
    // Inject Reflector so we can get metadata:
    private reflector: Reflector
  ) {}

  onApplicationBootstrap() {
    const modules = Array.from(this.modulesContainer.values());
    const modulesWithPathPrefix = modules
      .map((module) => {
        // Use the native `Reflect` API to get all decorator metadata
        // on the modules's `class` (eg, metatype):
        const metadataKeys = Reflect.getMetadataKeys(module.metatype);
        // Look for a metadata key so we can use it to get path metadata:
        const pathKey = metadataKeys.find((key) =>
          String(key).startsWith('__module_path__')
        );

        // If we found a path key, pull the path out:
        const path = pathKey
          ? this.reflector.get(pathKey, module.metatype)
          : undefined;
        return {path, module};
      })
      // Filter out all modules with no paths:
      .filter(({path}) => Boolean(path));

    console.log(modulesWithPathPrefix);
  }
}

// Run the program:
Promise.resolve().then(async () => {
  const app = await NestFactory.create(RouteDiscoveryAppModule);
  app.listen(3001);
});

It's pretty straight forward, but has the serious downside of relying on internal implementation details of Nest. If Nest decides to change how module paths are stored, they could do so at any time without bumping major version numbers.