Is there a method to retrieve and organize all routes and corresponding endpoints in a NestJS application?

When it comes to writing middleware to process requests, I am faced with the issue of excluding certain paths without having to hardcode them manually. To tackle this problem, I came up with an innovative solution:

My plan is to create a special decorator that can be used to tag methods as excluded, like this:

import { ReflectMetadata } from '@nestjs/common';
export const Exclude = () =>
  ReflectMetadata('exclude', 'true');

I am curious if there is a way to dynamically retrieve all methods annotated with this decorator after creating a NestJS application, so that their paths can be automatically added to the exclude list in my middleware. Any ideas on how to achieve this?

Answer №1

If you're looking to streamline the process of discovering metadata on your handlers or Injectable classes, I've created a reusable module designed specifically for this purpose. By utilizing @nestjs-plus/common from NPM and leveraging the DiscoveryService, you can automatically retrieve all relevant handlers or classes based on a MetaData token of your choosing. The source code for this module can be found on Github. I will be updating the documentation in the near future, but there are already numerous examples available in the repository.

Underneath it all, the module utilizes the MetaDataScanner but presents a user-friendly API for ease of use. This could potentially reduce a significant amount of boilerplate in your specific use case, as demonstrated in examples within the @nestjs-plus/rabbitmq module (also located in the same repository). These examples showcase how you can effectively utilize this functionality to enhance advanced features.

EDIT: I have recently updated the library to also support scenarios involving the discovery of controllers and controller methods. You can explore a comprehensive test suite that mirrors your setup using the @Roles decorator by following this link. After including the DiscoveryModule in your imports and injecting the DiscoverService, you can easily find all controller methods using the simplified

methodsAndControllerMethodsWithMeta
API.

// Inject the service
constructor(private readonly discover: DiscoveryService) { }

// Discover all controller methods decorated with guest roles or 
// belonging to controllers with guest roles

const allMethods = this.discover.methodsAndControllerMethodsWithMeta<string[]>(
  rolesMetaKey,
  x => x.includes('guest')
);

Once you have retrieved all the desired methods, you have the flexibility to manipulate them as needed—such as creating a collection based on their RequestMethod and path.

const fullPaths = allGuestMethods.map(x => {
  const controllerPath = Reflect.getMetadata(
    PATH_METADATA,
    x.component.metatype
  );

  const methodPath = Reflect.getMetadata(PATH_METADATA, x.handler);
  const methodHttpVerb = Reflect.getMetadata(
    METHOD_METADATA,
    x.handler
  );

  return {
    verb: methodHttpVerb,
    path: `${controllerPath}/${methodPath}`
  }
});

This would output results similar to those shown in the linked test suite:

expect(fullPaths).toContainEqual({verb: RequestMethod.GET, path: 'guest/route-path-one'});
expect(fullPaths).toContainEqual({verb: RequestMethod.GET, path: 'super/route-path-two'});
expect(fullPaths).toContainEqual({verb: RequestMethod.POST, path: 'admin/route-path-three'});

I welcome any feedback or insights regarding this approach/API.

Answer №2

Feel free to explore on your own.

Having delved into the depths of NestJS sources, I uncovered a pathway for those keen on learning:

import * as pathToRegexp from 'path-to-regexp';
import { INestApplication, RequestMethod } from '@nestjs/common';
import { NestContainer } from '@nestjs/core/injector/container';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { PATH_METADATA, MODULE_PATH, METHOD_METADATA } from '@nestjs/common/constants';

const trimSlashes = (str: string) => {
  if (str != null && str.length) {
    while (str.length && str[str.length - 1] === '/') {
      str = str.slice(0, str.length - 1);
    }
  }
  return str || '';
};

const joinPath = (...p: string[]) =>
  '/' + trimSlashes(p.map(trimSlashes).filter(x => x).join('/'));

// ---------------8<----------------

const app = await NestFactory.create(AppModule);

// ---------------8<----------------

const excludes = Object.create(null);
const container: NestContainer = (app as any).container; // this is "protected" field, so a bit hacky here
const modules = container.getModules();
const scanner = new MetadataScanner();

modules.forEach(({ routes, metatype }, moduleName) => {
  let modulePath = metatype ? Reflect.getMetadata(MODULE_PATH, metatype) : undefined;
  modulePath = modulePath ? modulePath + globalPrefix : globalPrefix;

  routes.forEach(({ instance, metatype }, controllerName) => {
    const controllerPath = Reflect.getMetadata(PATH_METADATA, metatype);
    const isExcludeController = Reflect.getMetadata('exclude', metatype) === 'true';
    const instancePrototype = Object.getPrototypeOf(instance);

    scanner.scanFromPrototype(instance, instancePrototype, method => {
      const targetCallback = instancePrototype[method];
      const isExcludeMethod = Reflect.getMetadata('exclude', targetCallback) === 'true';

      if (isExcludeController || isExcludeMethod) {
        const requestMethod: RequestMethod = Reflect.getMetadata(METHOD_METADATA, targetCallback);
        const routePath = Reflect.getMetadata(PATH_METADATA, targetCallback);

        // add request method to map, if doesn't exist already
        if (!excludes[RequestMethod[requestMethod]]) {
          excludes[RequestMethod[requestMethod]] = [];
        }

        // add path to excludes
        excludes[RequestMethod[requestMethod]].push(
          // transform path to regexp to match it later in middleware
          pathToRegexp(joinPath(modulePath, controllerPath, routePath)),
        );
      }
    });
  });
});

// you are now equipped to utilize the `excludes` map within middleware

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Filtering without the includes() Method

I have two Objects that are structured as follows: export const recipes: Recipe[] = [ new Recipe( id: "Green", scenario: ["1", "2"]), new Recipe( id: "Blue", scenario: ["1", "2","2"]) ]; export const scenarios: Scenario[] = [ new Scenario( id: "1 ...

Exploring the TypeScript Type System: Challenges with Arrays Generated and Constant Assertions

I am currently grappling with a core comprehension issue regarding TypeScript, which is highlighted in the code snippet below. I am seeking clarification on why a generated array does not function as expected and if there is a potential solution to this pr ...

Issues with running NPM script for compiling TypeScript code

[UPDATE] Initially, I resolved this issue by uninstalling tsc using npm uninstall tsc (as recommended in the response marked as the answer). However, the problem resurfaced after some time, and eventually, I found success by utilizing Windows Server for L ...

What is the correct way to properly enter a Svelte component instance variable?

Currently, I am delving into learning Svelte and specifically exploring how to bind to a component instance as demonstrated in this tutorial: As I progress through the tutorial, I am attempting to convert it to Typescript. However, I have encountered an ...

Display a targeted highcharts tooltip using React and typescript

In my React project with TypeScript, I am looking to have the Highchart tooltip appear when the chart is initially displayed. Specifically, I want it to show at a certain point on the chart. I understand that I will need to utilize a load function for thi ...

Guide to making a ng-bootstrap modal that retains a component's state

I am currently working on implementing a modal with Angular by following a tutorial on the ng-bootstrap website ( - in the "Components as content" section). However, I am facing a challenge where I want the component displayed in the modal to retain its st ...

What improvements can I implement in this React Component to enhance its efficiency?

Seeking advice on improving the efficiency of this React Component. I suspect there is code repetition in the onIncrement function that could be refactored for better optimization. Note that the maxValue prop is optional. ButtonStepper.tsx: // Definition ...

When using a Redux action type with an optional payload property, TypeScript may raise complaints within the reducer

In my react-ts project, I define the following redux action type: type DataItem = { id: string country: string population: number } type DataAction = { type: string, payload?: DataItem } I included an optional payload property because there are tim ...

Troubleshooting Issue with Angular Library: Live Reload Feature Not Functioning

In setting up my Angular workspace, I have 3 libraries and one application (with more to be added in the future). This is how the TypeScript paths are configured: "paths": { "@lib/a/*": [ "projects/libs/a/*", ...

Receiving intelligent suggestions for TypeScript interfaces declared within function parameters

Here is a simple example I put together: https://i.sstatic.net/Fdtfa.png In this example, intellisense provides suggestions for the interface of the object named test in the foo function. It works perfectly and I love it! However, if you declare that in ...

Encountering a Typescript issue with the updated Apollo server version within a NestJS application

After upgrading my nestJS application to use version 3.2 of apollo-server-plugin-base, I encountered two TypeScript errors related to a simple nestJS plugin: import { Plugin } from '@nestjs/graphql' import { ApolloServerPlugin, GraphQLRequest ...

Issue: Unrecognized element type in next.js while starting development server

Every time I run npm run dev, I encounter the following error: Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from th ...

Tips for resolving the unmounted component issue in React hooks

Any suggestions on resolving this issue: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect ...

TypeScript: "Implementing" methods. The organized approach

Currently, I am developing a middleware class for Express. Before delving into the details of the class, it's important to note that I will be injecting the middleware later by creating an instance of my "Authenticator" class and then using its method ...

Converting a promise of type <any> to a promise of type <entity>: A beginner's guide

As a newcomer to TypeScript and NestJS, I am wondering how to convert Promise<any[]> to Promise<MyEntity[]> in order to successfully execute the following code: const usersfromTransaction = this.repoTransaction .createQueryBuilder() ...

Discrepancies in ESLint outcomes during React app development

As a newcomer to React development, I am encountering discrepancies between the errors and warnings identified in my project's development environment versus its production environment. Strangely, I have not configured any differences between these en ...

How can I effectively utilize the Metamask SDK with TypeScript?

Currently, I am in the process of developing a webpack+pnpm+typescript+react website. All the versions being used are LTS and my preferred IDE is VSCode. According to the guide provided by Metamask here, it seems like I need to follow these steps: npm i @m ...

A TypeScript method for accessing deeply nested properties within an object

I'm currently working on a function that utilizes typings to extract values from a nested object. With the help of this post, I managed to set up the typing for two levels successfully. However, when I introduce a third (known) level between the exis ...

ReactJS and Redux: setting input value using properties

I am utilizing a controlled text field to monitor value changes and enforce case sensitivity for the input. In order to achieve this, I need to access the value property of the component's state. The challenge arises when I try to update this field ...

Ways to delete a class in typescript

There's a menu on my website with li tags containing an a element for navigation. Everything is working fine, but I'm facing an issue where I need to remove all elements with the class seleccionado and only add it to the clicked li. I tried using ...