Introduction
Before we talk about the actual implementation, let's first give some introduction about DTO, what does it mean, when to use and the real need for it in javascript/nodejs projects
What is DTO
DTO stands for data transfer object which is meant by defining a container that contains group of values or fields not methods that explains how the data should be passed around the layers. Some people mixes between defining database models and DTO, remember this sentence:
DTO is meant for operations and data transferring however models meant for data persistence.
When To Use DTO?
Alot of developers go to DTOs when starting developing complex applications in typescript/nodejs to represent their data and how they are transferred throw layers of the application, and this actually true and required, but I am coming today to tell you this is also required in javascript/nodejs development to prevent your code be sucks!!
Why DTO in Javascript?
Imagine that you have a high level dynamic language like javascript and developing rest APis using nodejs, you started creating your models, data validations for example using express-validator
and defined your routes, middlewares and everything is working fine. along the requirements are changing and you update the code frequently, you had multiple services and multiple APis consuming the same model in different ways and you are duplicating some fields in each service to pass them from controller layer to service layer and then to the layer which responsible on persisting data in database. After a while when you go to the code you will not understand what are data should be passed to the service layer and what are the data should be returned from this service, then here you need DTO.
Imagine also you are connecting to a firebase as a persistence database or document database without a strict schema and you have an endpoint that takes the data as json, doing some validations using express-validator
and pass these data to service layer and then this service layer passes these data to persistence layer, your required fields are something as the following:
{username: String, email: String, password: String}
how do you guarantee that the APi consumer can send more fields rather than the defined fields? for example the consumer of the APi can send the following data:
{
"username": "test",
"email": "test@gmail.com",
"password": "specificPass",
"birthDate": "2022-05-09T20:12:13.318Z"
}
do you see here? I am able to send fields not defined in the validations which will violate our service, and these data will be passed to the persistence layer and saved unspecified data in the database.
Suppose also if you have an APi, and web socket connection that are consuming the same service layer, how will you define your validations for both? you could endup with duplication in defining your exposed data in both!
In all these cases you need DTO. The Idea behind it is very simple, it gives you the ability to describe how do you receive data and expose data in your layers.
Implementation and Example
Initially we will define an expressjs route as the following:
router.post("/user/register", validations, registerController);
and we will have the validations using express-validator as the following:
const validations = [
body("username").exists().isString().notEmpty(),
body("email").exists().isEmail(),
body("password").exists().isString().notEmpty(),
]
Then you have the controller/handler as the following:
const registerController = (req, res) => {
const result = await userService.registerUser(req.body);
return res.status(200).json(result);
}
And your simple service layer as the following:
const registerUser = (userData) => {
userPersistenceLayer.add(userData);
}
Now let's define our basic DTO, but before that let me ensure on two facts:
- DTO is meant for data transferring and db model is meant for data persistence.
- Think about DTO as a contract that you use to treat with others using this contract specifications.And the contract specifications are the defined fields inside it
class RegisterUserDTO{
username;
email;
password;
constructor(data) {
this.username = data.username;
this.email = data.email;
this.password = data.password;
}
}
Then we can go back to the service layer and use our defined DTO
const registerUser = (userData) => {
userPersistenceLayer.add(new RegisterUserDTO(userData));
}
As you can see with this pattern we are controlling the way we pass data and ensure which fields are being passed to other layers, also we can set some getters and setters in this DTO to serialize/transform some data as needed.
Hope this reached you in a clean and smooth clarification for DTO pattern.
Top comments (1)
This was really insightful. Please I'm relative new and would love to know what "best/industry practice" folder structure/pattern looks like for js backend development.