How to generate generic DTOs with NestJs and Swagger

August 23, 2023 ☼ NestJSNode.jsSwagger

A DTO (Data Transfer Object) is a design pattern used to encapsulate and transfer data between different layers or components of an application. The primary purpose of a DTO is to provide a standardised and efficient way of transmitting data without exposing the underlying implementation details.

A DTO is essentially a simple container for data that is meant to be passed around between different parts of an application, often between the frontend and backend or between different layers within the backend.

Why use DTOs?

  1. Data Integrity and Security: control the data that is transferred between different layers of the application.
  2. Data Validation: validate the data before sending it to the next layer
  3. Versioning and Evolution: help managing versioning and ensure backward compatibility by allowing different versions of the DTO to coexist or by providing mechanisms to map between different versions.
  4. Performance: reduce the amount of data that needs to be sent across the wire in distributed applications.
  5. Reduced Overhead: send only the required data instead of sending complete domain entities with all their associated properties

DTOs in NestJS

In NestJS you can define a DTO by creating a class with the properties you want to expose. For example:

export class CreateUserDto {
  @IsString()
  name: string;
  @IsEmail()
  email: string;
}

Then you can use it in your controller:

@Post()
async create(@Body() createUserDto: CreateUserDto) {
  return 'Ok';
}

In this case NestJS will automatically validate the request body against the DTO and will return a 400 Bad Request if the validation fails.

What about the Response DTO?

You can use the same approach:

export class ResponseUserDto {
  id: number;
}
@Post()
create(@Body() createUserDto: CreateUserDto): Promise<ResponseUserDto> {
  return this.myUserService(createUserDto); // Will return a Promise<ResponseUserDto>
}

DTOs with Swagger

Swagger is a set of open-source tools built around the OpenAPI Specification that can help you design, build, document and consume REST APIs.

NestJS has a built-in integration with Swagger that allows you to automatically generate the OpenAPI specification and Swagger UI for your application.

You can find more information about Swagger and NestJS here.

Let’s see how we can apply the same approach we used before to generate the DTOs from the Swagger specification.

I assume you have installed the @nestjs/swagger package.

export class CreateUserDto {
  @ApiProperty({
    example: "User name",
    description: "Name of your user",
  })
  @IsString()
  name: string;

  @ApiProperty({
    example: "User Email",
    description: "A valid email address",
    example: "user@gmail.com",
  })
  @IsEmail()
  email: string;
}

Similarly for the response DTO:

export class ResponseUserDto {
  @ApiProperty({
    example: f1ef0d90-7cf5-46e5-ab8e-f676ab14ff2d,
    description: 'The id of the user',
  })
  id: number;
}

Pretty neat and simple ah?

A common scenario

Imagine that your API has a lot of endpoints and for all the GET requests you have some standard fields like pagination, count, etc. that every DTO must include. You can include them manually in each DTO but it’s a tedious and error-prone task.

What if there’s a new field to be added to the standard response? You have to update all the DTOs manually.

A better approach: Generics

You would think that something like this should work:

class BaseResponseDTO<T> {
    @ApiProperty({
      description: 'Numbers of returend items',
      example: 2,
    })
    @IsNumber()
    count: number;

    pagination: {
      @ApiProperty({
        description: 'The current page',
        example: 1,
      })
      @IsNumber()
      page: number;

      @ApiProperty({
        description: 'The number of items per page',
        example: 10,
      })
      @IsNumber()
      limit: number;
    };

    @ApiProperty({
      description: 'The data',
      type: [T],
    })
    data: T[]
  }

Unfortunately this doesn’t work. You won’t be able to have correct definition generated. nestjs/swagger uses TypeScript reflection capabilities, and unfortunately, TypeScript reflection doesn’t work with generics.

Enter Mixins

A mixin is a class that contains methods for use by other classes without having to be the parent class of those other classes. In other words, a mixin provides methods that implement a certain behaviour, but we do not use it alone, we use it to add the behaviour to other classes.

Read the official documentation for more information about mixins.

Let’s see how we can use mixins to solve our problem.

import { mixin } from '@nestjs/common';
import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNumber, ValidateNested } from 'class-validator';

type Constructor<T = {}> = new (...args: any[]) => T;

export function withBaseResponse<TBase extends Constructor>(
  Base: TBase,
  options?: ApiPropertyOptions | undefined,
) {
  class ResponseDTO {
    @ApiProperty({
      description: 'Numbers of returend items',
      example: 2,
    })
    @IsNumber()
    count!: number;

    pagination!: {
      @ApiProperty({
        description: 'The current page',
        example: 1,
      })
      @IsNumber()
      page: number;

      @ApiProperty({
        description: 'The number of items per page',
        example: 10,
      })
      @IsNumber()
      limit: number;
    };

    @ApiProperty({
      isArray: true,
      type: Base,
      ...options,
    })
    @Type(() => Base)
    @ValidateNested({ each: true })
    data!: Array<InstanceType<TBase>>;
  }
  return mixin(ResponseDTO); // This is important otherwise you will get always the same instance
}

As you can see we are using the mixin function provided by NestJS to create a new class that extends the Base class and adds the new properties.

Now we can use it in our DTOs:

export class ResponseUserDto extends withBaseResponse(UserDto, {
  description: "List of returned Users",
  example: [
    {
      id: "159b79f3-98dc-4d1b-9472-05a3b02f51bb",
    },
  ],
}) {}

If you check the generated Swagger definition you will see that the ResponseUserDto will have the correct definition.

Note: I usually validate the DTOs using the class-validator package. Whenever the data changes from layer to layer I want to be sure that the data is valid.

That’s all folks!


If you have any suggestions, questions, corrections or if you want to add anything please DM or tweet me: @zanonnicola