Blog Cover

How to generate Generics DTOs with nestjs/swagger

Author profile image
Aitor Alonso

Dec 04, 2021

2 min read

I recently had to face with such a problem while developing a REST API for a customer's project at work. I was providing endpoints to populate multiple table views, of different kind of data, with pagination. We use Nest.js and Typescript to develop the API, and we rely on the auto-generated Swagger documentation to keep the API docs up to date.

So I wanted to use a generic PaginatedResponseDto like this:

export class PaginatedResponseDto<T> {
  @ApiProperty()
  data: T[]
  @ApiProperty()
  totalCount: number
  @ApiProperty()
  offset: number
  @ApiProperty()
  limit: number
}

But Swagger won't be able to extract anything from T to generate the correct OpenAPI schema definition. That's because nestjs/swagger uses TypeScript reflection capabilities, and unfortunately, TypeScript reflection doesn't work with generics.

However, there is a way to write our own raw OpenAPI schema definitions, and we can use that to achieve what we want. First, we remove the @ApiProperty decorator for the data attribute from the above DTO, as we will provide its own schema definition later on.

export class PaginatedResponseDto<T> {
  data: T[]
  @ApiProperty()
  totalCount: number
  @ApiProperty()
  offset: number
  @ApiProperty()
  limit: number
}

Now, we can create a custom decorator to use on the endpoints that will return a PaginatedResponseDto. In my case, it was named ApiOkResponsePaginated:

export const ApiOkResponsePaginated = <DataDto extends Type<unknown>>(dataDto: DataDto) =>
  applyDecorators(
    ApiExtraModels(PaginatedResponseDto, dataDto),
    ApiOkResponse({
      schema: {
        allOf: [
          { $ref: getSchemaPath(PaginatedResponseDto) },
          {
            properties: {
              data: {
                type: 'array',
                items: { $ref: getSchemaPath(dataDto) },
              },
            },
          },
        ],
      },
    })
  )

A little explanation about what this does.

  • The custom decorator receives as the parameter dataDto the DTO class that will replace T in our generic DTO.
  • The ApiExtraModels decorator from nestjs/swagger indicates swagger to generate and construct the OpenAPI schema for both DTOs, our PaginatedResponseDto and whatever DTO we will use to type the data attribute.
  • allOf is for OpenAPI 3 to cover inheritance use-cases.
  • We use getSchemaPath to retrieve the PaginatedResponseDto OpenAPI schema, and then we fine-tune its data attribute, indicating that it's of type array and that the OpenAPI schema for its items is the one generated for dataDto.

And this is how we will use it for, for example, a products list endpoint for an e-commerce:

@Controller('products')
export class ProductsController {

  @Get('/')
  @ApiOkResponsePaginated(ProductDto)
  async get(): Promise<PaginatedResponseDto<ProductDto>> {}
}

Now, you can check your Swagger auto-generated documentation and see that everything is working as expected. Hurrah!

This is the very basis to make generic DTOs works. The ApiOkResponsePaginated decorator can be customized as needed, like for example adding a description attribute inside the data: {} object to provide a description about the data returned. Whatever nestjs/swagger allows you to do, can be done here, so you can make sure your OpenAPI spec is correct and covered.