August 23, 2023 ☼ NestJS ☼ Node.js ☼ Swagger
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.
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>
}
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?
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.
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.
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