Welcome to this lesson on transforming data store objects to Data Transfer Objects (DTOs) in NestJS. So far, we've covered integrating MongoDB into your NestJS app, from installing and configuring MongoDB and Mongoose, to defining schemas for ToDo
items and implementing the necessary service and controller layers for CRUD operations. In this lesson, we're going to focus on how to use DTOs with database objects, a crucial part of managing data flow efficiently in your application. We'll also introduce class-transformer
, the library we'll use for transforming data, and explore how to implement these transformations in your NestJS application.
Right now, our application is returning the data from the database exactly as Mongo gives it to us. That is a problem because the representation that Mongo stores data in is not always (often not, in fact!) the same representation that we want to expose from our API. For example, a Todo from our API currently looks like this:
JSON1{ 2 "title": "Learn NestJS", 3 "description": "Follow the course to learn NestJS", 4 "completed": false, 5 "_id": "66faff8a9dd50cd4e36391e2", 6 "__v": 0 7}
That looks mostly correct except for the following issues:
- We don't want
_id
. Instead, we wantid
- What is
__v
? That is an implementation detail of Mongo that we don't want to expose to the consumers of the API
We'd like the JSON to look more like this:
JSON1{ 2 "id": "66faff8a9dd50cd4e36391e2", 3 "title": "Learn NestJS", 4 "description": "Follow the course to learn NestJS", 5 "completed": false 6}
Data Transfer Objects (DTOs) are simple objects used to transfer data between different parts of an application. DTOs are especially useful in web applications for three main reasons:
- Validation: They ensure the data being transferred meets certain criteria.
- Transformation: They convert data from one format or structure to another.
- Encapsulation: They group related data together into a single object.
You used DTOs in the previous courses and we'll use them again. But how do we best return a DTO when whate we have is a Database Object?
It is a common pattern in NestJS to use a library called class-transformer
for transforming data between objects. This library provides decorators and utility functions to transform plain JavaScript objects into class objects.
First, let's make sure you have the necessary libraries installed. If you're working on your local machine, you can install them using npm:
Bash1npm install class-transformer
In the CodeSignal environment, these libraries are pre-installed, so you won't need to worry about installation here.
Let's modify our three DTO classes for our ToDo
application: TodoDto
, CreateTodoDto
, and UpdateTodoDto
.
TypeScript1import { Expose, Exclude } from 'class-transformer'; 2 3// Exclude fields unless explicitly stated below 4@Exclude() 5export class TodoDto { 6 7 // Convert the Mongo _id to the DTO id field 8 @Expose({name: '_id'}) 9 id: string; 10 11 @Expose() 12 title: string; 13 14 @Expose() 15 description: string; 16 17 @Expose() 18 completed: boolean; 19} 20 21@Exclude() 22export class CreateTodoDto { 23 @Expose() 24 title: string; 25 26 @Expose() 27 description: string; 28} 29 30@Exclude() 31export class UpdateTodoDto { 32 @Expose() 33 title?: string; 34 35 @Expose() 36 description?: string; 37 38 @Expose() 39 completed?: boolean; 40}
@Exclude()
: This decorator ensures that all class properties are excluded from serialization unless explicitly exposed.@Expose()
: This decorator makes specific properties available during serialization.
In the example above:
TodoDto
transforms the MongoDB_id
to a more friendlyid
and exposes relevant properties liketitle
,description
, andcompleted
.TodoDto
will not expose the__v
fieldCreateTodoDto
andUpdateTodoDto
handle new and update requests, respectively.UpdateTodoDto
supports optional properties due to the?
symbol.
First, we define a utility function, transformTodoDto
, to transform our document to a DTO.
TypeScript1import { plainToClass } from 'class-transformer'; 2import { TodoDocument } from './schemas/todo.schema'; 3import { TodoDto } from './dtos/todo.dto'; 4 5function transformTodoDto(todo: TodoDocument): TodoDto { 6 return plainToClass(TodoDto, todo.toJSON()); 7}
Next, let's update our controller methods to use this utility function for transforming data.
TypeScript1import { Controller, Get, Query, Param, Post, Body, Put, Delete, NotFoundException } from '@nestjs/common'; 2import { TodoService } from './todo.service'; 3import { TodoDto, CreateTodoDto, UpdateTodoDto } from './dtos/todo.dto'; 4 5@Controller('todos') 6export class TodoController { 7 constructor(private readonly todosService: TodoService) {} 8 9 @Get() 10 async findAll(@Query('showIncomplete') showIncomplete: boolean): Promise<TodoDto[]> { 11 const todos = await this.todosService.findAll(showIncomplete); 12 return todos.map(transformTodoDto); 13 } 14 15 @Get(':id') 16 async findOne(@Param('id') id: string): Promise<TodoDto> { 17 const todo = await this.todosService.findOne(id); 18 19 if (!todo) { 20 throw new NotFoundException('Todo not found'); 21 } 22 23 return transformTodoDto(todo); 24 } 25 26 @Post() 27 async create(@Body() todo: CreateTodoDto): Promise<TodoDto> { 28 const newTodo = await this.todosService.createTodo(todo); 29 return transformTodoDto(newTodo); 30 } 31 32 // etc 33}
In this lesson, we explored the concept of Data Transfer Objects (DTOs) and how to implement them using the class-transformer
library in a NestJS application. We discussed the creation of DTO classes (TodoDto
, CreateTodoDto
, UpdateTodoDto
) and how to use decorators such as @Exclude
and @Expose
to manage data serialization. We then saw how to apply these transformations within the controller methods and walked through practical examples for creating, updating, and fetching todos
.
You've completed a significant part of your NestJS learning journey! Now it's time to put theory into practice with hands-on exercises that reinforce these concepts.