Proper Way To Create Response DTO in Nest.js

Ayuth Mangmesap
Ayuth’s Story
7 min readMar 1, 2024

--

When it comes to requesting Nest.js. There are plenty of tutorials about how to create request DTO and for validation, but there are no concrete examples for accomplishing that.

After spending hours of research and testing, I decided to write a blog post to share this and what I have learned in some practices.

What is DTO?

In simple terms, DTO is an object that represents data transfer between client and server. Let’s take a look at its benefits and why.

Why Response DTO?

  • Control Over Response Data: Response DTOs allow you to have fine-grained control over the data that is sent back to clients.
    We can specify exactly which fields should be included in the response, omit the sensitive information, or transform the data to suit the client’s needs.
  • Consistency: Using response DTOs across your API ensures a consistent data structure in your responses. With this consistency, it’s easier for clients to understand the format of the data they’ll receive, reducing ambiguity and potential errors
  • Documentation: Response DTOs serve as a form of documentation for our API and make it clear what data is expected in the request and response.
  • Type Safety: If you’re using the mono repo which contains web and API, we can simply reference to response DTO which helps catch bugs earlier when the response is changed.

🧑‍💻 Create Response DTO in Nest.js

Assume that we have an User entity that represents a user in the database or any source. In this blog, I’ll keep it in a simple array called users

class User {
id: string;
firstName: string;
lastName: string;
password: string;
}

🔧 The Global Settings

Before going through let’s put some settings

// 📄 src/main.ts

import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { ClassSerializerInterceptor, INestApplication } from '@nestjs/common';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
registerGlobals(app);
await app.listen(3000);
}

export function registerGlobals(app: INestApplication) {
app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector), {
// strategy: 'excludeAll', 👈 we'll talk about this later
}),
);
}

bootstrap();

with the global settings, you can specify the strategy that will affect the entire application.

This makes our behavior consistent along the way and doesn’t need to specify every class or controller.

🤖 The Service

Service is a fundamental building block of an application that encapsulates the business logic and performs specific tasks or operations.

To keep the blog simple, I’d rather not connect to any external sources, and instead, use a local variable. In the real world, you might get data from other services, repositories, or external data sources.

import { Injectable } from '@nestjs/common';
import { User } from './entities/user.entity';

const users: User[] = [
{
id: '1',
firstName: 'John',
lastName: 'Doe',
password: '123456789',
},
];

@Injectable()
export class UsersService {
async getUsers() {
return users;
}
}

The Response DTO

As described, the response allows us to control the data that is sent back to clients. In the user entity, we have password field which must not be sent to a client.

Let’s say we create a simple class called UsersResponseDto to represent the object and specify only the client's needs

// 📄 src/users/dto/users-response.dto.ts
import { Expose } from 'class-transformer';

export class UsersResponseDto {
@Expose()
id: string;

@Expose()
firstName: string;

@Expose()
lastName: string;
}

You may notice that we marked a property that we want to expose to the client with Expose decorator and you’re correct 😁.

When the controller returns an object, the mapper method (which we’ll see below) will tell the class-trasnformer to exclude or expose a property

  • @Expose — tells the class-transformer that we want to expose this property when mapping
  • @Exclude — tells the class-transformer that we don’t want to expose this when mapping

and the response will be

[
{
"id": "1",
"firstName": "John",
"lastName": "Doe",
"password": "123456789"
}
]

Why the password doesn’t exclude . Because we didn’t have a flag to tell them class-transformer that we don’t want to expose this.

You may think … hmm. If I don’t want a property such as user.password Do I need to define the password property and mark it as @Exclude.

Indeed, we can do it that way but I do not recommend that because we have a better solution for this.

We can simply put @Exlucde on top of the class to ensure that we don’t expose any property unless we define it

For instance:


import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class SomeResponseDto {
@Expose()
id: string;

// ✂️ ...
}

or in the global settings, you might define startegy to excludeAll as default.

This setting ensures that every property is excluded by default unless you mark it as exposed.

app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector), {
strategy: 'excludeAll',
}),
);

or to be more precise, you can add them excludeExtraneousValues: true to tell the class-transformer that please exclude properties not part of the original class.

app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector), {
strategy: 'excludeAll',
excludeExtraneousValues: true,
}),
);

The Controller

The controller is responsible for handling incoming requests and generating appropriate responses.

We’ll create a controller that will return all of the users in our service with the appropriate response that we defined in our response to

// 📄 src/users/users-controller.ts
// ... ✂️
import { UserResponseDto } from './dto/users-response.dto.ts'


@Controller('/users')
export class UsersController {
// ... ✂️
@Get()
async getUsers(): Promise<UsersResponseDto[]> {
const users = await this.usersService.getUsers();
return plainToInstance(UsersResponseDto, users); // 👈 this is our key mapping method
}
}

The key method is plainToInstance a method that will map the array users to the given object. In this case, it’s the UserResponseDto. And the final result will be:

[
{
"id": "1",
"firstName": "John",
"lastName": "Doe"
]
}
]

Nested Response

In some cases, you might want to return a nested response, for instance, the user are many todos and we want to return in a single request.

We can simply create our response DTO like this

import { Expose, Type } from 'class-transformer';

export class TodoResponse {
@Expose()
id: string;

@Expose()
completed: boolean;
}

export class UsersResponseDto {
// ✂️ ...
@Type(() => TodoResponse) // 🔑 don't forget to put this
@Expose() // also the @Expose()
todos: TodoResponse[];
}t

If we forget Type(() => <...>) then the response will return as an empty object {} . So we need to tell class-transformer about the type we will map to. And the result will be

[
{
"id": "1",
"firstName": "John",
"lastName": "Doe",
"todos": [
{
"id": 1,
"completed": false
}
]
}
]

All the code from the example is available below except the nested type part.

Swagger

Swagger is an open-source toolset that helps developers design, build, document, and consume RESTful web services. It provides a powerful interface to visually interact with your API and understand its capabilities.

NestJS has the @nestjs/swagger module which helps us integrate seamlessly with the Nest.js application

First, install the swagger

$ npm install --save @nestjs/swagger

Then apply Swagger plugin in nest-cli.json. This helps us to not decorate the @ApiProperty() to let it show in the Swagger doc on top of every property

// nest-cli.json
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
"plugins": ["@nestjs/swagger"]
}
}

Create a setup.ts besides main.ts for storing the setup of our app

// setup.ts
import { ClassSerializerInterceptor, INestApplication } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

export function setup(app: INestApplication) {
app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector), {
strategy: 'excludeAll',
}),
);

const config = new DocumentBuilder()
.setTitle('API Example')
.setDescription('The API description')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);

return app;
}

Then import in the main.ts and pass the app to get the proper setup

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { setup } from './setup';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
setup(app);
await app.listen(3000);
}
bootstrap();

You’ll get a result like this

Sidenote For Testing

The response might not be matched to the response DTO for the testing controller.

This is because we didn’t apply the global settings to our initiation of testing unless the result will not be the same as intended.

We have several ways to solve this

  1. Apply every @Exclude decorator to every response to class.
  2. Apply the strategy: ‘excludeAll’ to global ClassSerializerInterceptor .
  3. Separate the settings to separate methods and apply them when initializing the test.

I’d suggest going on the second route which helps us prevent unintended exposure unless we want to.

Conclusion

In this blog, we know what are the benefits of DTOs and how to create a response in Nest.js using class-transformer and apply to our project.

DTOs offer several benefits that can improve the maintainability, scalability, and security of your Nest.js application.

The working prototype is on GitHub

Any suggestions are welcome. 🙏

If you are interested in data transfer objects. Microsoft has written a specific article about this 👇

References

--

--