How to generate Generics DTOs with nestjs/swagger
3 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 replaceT
in our generic DTO. - The
ApiExtraModels
decorator fromnestjs/swagger
indicates swagger to generate and construct the OpenAPI schema for both DTOs, ourPaginatedResponseDto
and whatever DTO we will use to type thedata
attribute. allOf
is for OpenAPI 3 to cover inheritance use-cases.- We use
getSchemaPath
to retrieve thePaginatedResponseDto
OpenAPI schema, and then we fine-tune itsdata
attribute, indicating that it's of typearray
and that the OpenAPI schema for its items is the one generated fordataDto
.
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.