Search code examples
typescriptnestjsswaggerswagger-uinestjs-swagger

How to fully customize a Param decorator in NestJS to display to right properties in SwaggerUI


I'm working on a little NestJS project to create and manage events and tickets.

So, I'm using UUIDs in request parameter of my controller like this to get, update and delete my entities in my database:

@Get(':eventId')
async findOne(
  @UserId() userId: string,
  @Param('eventId') eventId: string,
): Promise<EventEntity> {
  return this.eventService.findOne(userId, eventId);
}

Until this point, there is no issue.

Then, I implemented a custom decorator to validate the UUID format. Here is the code snippet:

export const IsUUIDParam = createParamDecorator(
  (data: string, ctx: ExecutionContext): string => {
    const request = ctx.switchToHttp().getRequest();
    const uuid: string = request.params[data];

    if (!uuid) {
      return uuid;
    }

    // This regex checks if the string is a valid UUIDv4
    const cmp: RegExpMatchArray = uuid.match(
      '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
    );
    if (cmp === null) {
      throw new BadRequestException(`Invalid ${data} format`);
    }

    return uuid;
  },
);

And I use the new decorator like this in my controller:

@Get(':eventId')
async findOne(
  @UserId() userId: string,
  @IsUUIDParam('eventId') eventId: string,
): Promise<EventEntity> {
  return this.eventService.findOne(userId, eventId);
}

The custom decorator works well, but right now, the the Swagger does not display the required parameter.

Screenshot of Swagger not displaying the required parameter

So I followed this Stack Overflow post to implement the documentation on my custom decorator.

Here is my new custom decorator:

export const IsUUIDParam = createParamDecorator(
  (data: string, ctx: ExecutionContext): string => {
    const request = ctx.switchToHttp().getRequest();
    const uuid: string = request.params[data];

    if (!uuid) {
      return uuid;
    }

    // This regex checks if the string is a valid UUIDv4
    const cmp: RegExpMatchArray = uuid.match(
      '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
    );
    if (cmp === null) {
      throw new BadRequestException(`Invalid ${data} format`);
    }

    return uuid;
  },
  [
    (target, key): void => {
      // The code below define the Swagger documentation for the custom decorator
      const explicit =
        Reflect.getMetadata(DECORATORS.API_PARAMETERS, target[key]) ?? [];

      Reflect.defineMetadata(
        DECORATORS.API_PARAMETERS,
        [
          ...explicit,
          {
            in: 'path',
            name: 'uuid',
            required: true,
            type: 'string',
          },
        ],
        target[key],
      );
    },
  ],
);

But now, the Swagger documentation only display uuid:

Screenshot of Swagger displaying uuid instead of the required parameter

But I want to display eventId or the name of the parameter in a generic way (for example ticketId somewhere else in another controller).

I tried to get something from the target and key properties, but I didn't find anything. I didn't find anything neither on Internet nor with ChatGPT, and the data property is not accessible in the second parameter of the createParamDecorator() method where I'm trying to custom the Swagger documentation.

Do you know how can I fix my issue?


Solution

  • I finally found the answer.

    We can fix this issue by encapsulating the createParamDecorator() function in an arrow function taking a string parameter (here the data parameter).

    export const IsUUIDParam = (data: string) => // new arrow function that takes a string 
      createParamDecorator(
        // the object _ allow us to declare to our IDE that the parameter won't be used, and so it doesn't display a warning message
        (_: string, ctx: ExecutionContext): string => {
          const request = ctx.switchToHttp().getRequest();
          const uuid: string = request.params[data];
    
          if (!uuid) {
            return uuid;
          }
    
          // This regex checks if the string is a valid UUIDv4
          const cmp: RegExpMatchArray = uuid.match(
            '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
          );
          if (cmp === null) {
            throw new BadRequestException(`Invalid ${data} format`);
          }
    
          return uuid;
        },
        [
          (target, key): void => {
            // The code below define the Swagger documentation for the custom decorator
            const explicit =
              Reflect.getMetadata(DECORATORS.API_PARAMETERS, target[key]) ?? [];
    
            Reflect.defineMetadata(
              DECORATORS.API_PARAMETERS,
              [
                ...explicit,
                {
                  in: 'path',
                  // use the new data object here
                  name: data,
                  required: true,
                  type: 'string',
                },
              ],
              target[key],
            );
          },
        ],
      // Do not forget to add the parenthesis at the end to execute the arrow function
      )();
    

    Thanks to that, the data object (containing the UUIDv4 string) is accessible everywhere in the arrow function and so, in the second part of the createParamDecorator() function.

    We can change the first argument of the createParamDecorator() function by an underscore (_) to avoid warning messages in our IDE as we don't use this parameter anymore.

    We can then update the name property in our decorator with data to display the given name (in a dynamic way). Finally, add parenthesis at the end of the arrow function to execute it (())

    Nothing changes in the controller, we can still call our custom decorator with the following code :

    @Get(':eventId')
    async findOne(
      @UserId() userId: string,
      @IsUUIDParam('eventId') eventId: string,
    ): Promise<EventEntity> {
      return this.eventService.findOne(userId, eventId);
    }
    

    Here is a screenshot of the Swagger result for the event entity:

    Screenshot of the Swagger result - GET /event/:eventId

    And for the ticket entity:

    Screenshot of the Swagger result - GET /ticket/:ticketId

    Note: if you want to customize even more the decorator (oto add a custom description for example), you can add a parameter after data in the function prototype, give a second parameter when calling the function in the controller and finally use the new parameter wherever you want in the arrow function.

    And voilà! We can display the string parameter of our custom decorator in a dynamic way in Swagger.