Ajitabh
Ajitabh

Reputation: 891

Why do we need DTOs and interfaces both in NestJS

The NestJS documentation showcases how to add DTOs to use in Controllers to validate request objects by using class-validator package. DTOs described there are TypeScript classes. Now, while controllers deal with DTOs(TS Classes), NestJS providers (or services), on the other hand, makes use of TypeScript interfaces. These DTOs and interfaces are pretty much of the same shape.

Now, I am seeing duplication of shape definition here. And wondering if the interfaces are needed at all?

Can we not make DTOs source of truth for the shape and validations? One of the approaches we were considering (to make the DTO source of truth) was, to have an openapi generator take the DTOs as input and generate openapi definition and from there another codegen can generate a set of typescript interfaces to be consumed by NestJS itself and which can be shared with another set of consumer applications like Angular too.

Have any people come across a similar problem? What do you think of the above?

Upvotes: 89

Views: 53255

Answers (22)

Ahmet Emrebas
Ahmet Emrebas

Reputation: 945

The words consistency, extendability, effective collaboration, intuitivity might explain why we need dtos and interfaces.

Most of the time we do not need multiple dtos. For example, an entity with a single property name does not require more than one dto.

But let's think about user entity that belongs to an organization.

export interface User<Organization> { 
  username:string; 
  organization:Organization; 
}

How does the user dto look like?

export class CreateUserDto implements User<IID> { 
  username:string; 
  organization:IID
}

Should the dto have organization property?

The answer is YES if the request is made by root user.

But NO for clients. Let's say I am an administrator of an organization, then I do not need to provide organization input because the system already knows who I am and to which organization I belong. So, we need another dto for client users to empower security, especially data integrity. And, it looks like the following snippet.


export class CreateClientUserDto { 
  username: string; 
}

If we use the same dto, it might create an undesired data bridge between multiple organizations.

Also, It might confuse your teammates, they might think they need to pass the organization property and write the code accordingly.

Upvotes: 0

Artiphishle
Artiphishle

Reputation: 886

Only reason is to not glue the data model to the code, if you need the same data as in db everytime then you just "import type { YourDTO } as a type and you're fine, but best practice (as there is always something you want to change) is to have an interface too, and use it after applying a 'selector' (like redux selector, you want e.g. not the stored timestamp, but a readable date format)

This means you add one serialising/deserialising layer where you e.g. (get ready for worst example ever, but you will get the point)

// You store the timestamp in the database

DTO DateDto {
  date: 1711314253460
}

// But you really just need to work with the day and only in this format

interface IDate {
  date: "2024/03/02"
}

So you have now a proper mapping layer where incoming data (maybe even from 3rd party systems who have some id's as number/string/anotherFormatToComplicateThings, then you can just map it to whatever make sense in your project. There is NOWAY to accept depending on 3rd party systems and write your project according to its definitions.

Unfortunately it's very common wrong practice to create types randomly and on random locations instead of adding a proper layer directly at the db.

Everything after that (codebase) will use the defined types from there or EVERYONE in the team (especially new onboarding members) have the greatest PITA (pain...) ever felt. Promise.

Good luck! ;)

Upvotes: 0

Milad Ashoori
Milad Ashoori

Reputation: 61

DTOs are used in the edge of APIs, so they can have some information about the validation (message, ...) and are used in documentation (OpenAPI, ...) but interfaces' main usage is Typing and they are used for communication between domains in for example backend APP

Upvotes: 0

Reason for using DTO and Interface in NestJS

Basically, in rest API, we have two types of operation, One is Input and Another is Output. which is Request and Response

During response, we don't need to validate the return value. We just need to pass data based on the interface

But in request, we need to validate the body

for example, you want to create a user. Then the request body might be something like this

const body = {
 name: "Test Name",
 email: "[email protected]",
 phone: "0393939",
 age: 25
}

so during request we need to validate email, phone number or password is matched regex etc.

so in DTO we can do all validation

Here is one of my DTO example

import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class RegisterUserRequest {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsNotEmpty()
  @MinLength(6)
  password: string;
}

export class LoginUserRequest {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsNotEmpty()
  @MinLength(6)
  password: string;
}

And here is the interface example

import { UserRole } from './user.schema';

export interface UserType {
  _id?: string;
  email?: string;
  name?: string;
  role: UserRole;
  createdAt?: Date;
  updatedAt?: Date;
}

Hope you understand.

Thanks

Upvotes: 26

mostafa jafarzadeh
mostafa jafarzadeh

Reputation: 1

One of the reasons for using dto is that it checks the input so that valid data is entered One of the reasons for using the interface is that it helps us to comply with the Liskov substitution principle, which is related to solid.

Upvotes: 0

Vladimir Prudnikov
Vladimir Prudnikov

Reputation: 7232

IMO, this has nothing to do with NestJS or any other framework. This is clean architecture proposed and nicely described in his books by Robert Martin.

Clean Architecture

DTOs sits in WEB/UI (outer most) layer, while entities (or like you are referring to them as Interfaces) are in the core layer of the clean architecture.

It is a coincidence that you have them in pretty similar shape, most likely you have a small application and at this stage you will hardly see the value from this separation. But the more the app grows the more difference you will see and more important this separation will become.

Imagine you have a form on the UI that creates something, let's say blog post. In this form you will have Author, Tags, Header image, and of course blog post content. All this can be in a single DTO object, you can validate this object as a whole (all required and valid things provided). But at the controllers level, you will construct multiple Tag entities, Author Entity, ImageHeaderFile entity, BlogPost entity and work with them. You don't want you lower level components to depend on higher level components.

Here are a couple links to start with:

Upvotes: 1

Monirul Islam
Monirul Islam

Reputation: 3

In my opinion, DTO = Data Transfer Object. Dtos are like interfaces but their whole goal is to transfer data and validate it. They are mostly used in routers/controllers.

You can simplify your API body and query validation logic by using them. For instance, you have a AuthDto which automatically maps the user email and password to an object dto to enforce validations.

Where as the interface is just to declare how your response or a particular data model will be.

Upvotes: 0

Abdelraouf dz
Abdelraouf dz

Reputation: 31

I think the NestJs documentation answered this precisely:

A DTO is an object that defines how the data will be sent over the network. We could determine the DTO schema by using TypeScript interfaces, or by simple classes. Interestingly, we recommend using classes here. Why? Classes are part of the JavaScript ES6 standard, and therefore they are preserved as real entities in the compiled JavaScript. On the other hand, since TypeScript interfaces are removed during the transpilation, Nest can't refer to them at runtime. This is important because features such as Pipes enable additional possibilities when they have access to the metatype of the variable at runtime.

Link to this pragraph: https://docs.nestjs.com/controllers#request-payloads

Upvotes: 1

gridlock
gridlock

Reputation: 1

DTOs represent the structure of data transferred over the network it is meant to be for a specific use case whereas interfaces are more generalized specificity helps with better readability and optimizations. Interfaces don't exist after transpiling but nest accommodates dtos to be useful even after the transpilation face.

Upvotes: 0

Alesko
Alesko

Reputation: 17

For example, if the Dto you created for the incoming request needs to be checked for the incoming data, the best way to do this is to create the Dto as a class. Because after typescript is compiled, they continue to exist in your javascript code. In this way, you can add various validations. For example "IsNotEmpy", "IsString" etc. If the data doesn't need to be validated, you can create Dto using interface. So here, rather than a single correct or correct method, it's about what you need.

Upvotes: 1

Cody Breene
Cody Breene

Reputation: 41

I read through all of the answers, and none really seem to answer your question. I believe that yes, although DTOs and interfaces serve different purposes, I don't see a reason why you need to create an additional interface for typing purposes.

Happy to be proven wrong here, but none of the answers really address the OP's point, which is that the DTO serves a validation purpose, but you also get the typing for free.

Upvotes: 3

gmmiso88
gmmiso88

Reputation: 17

Basically you can validate request input without Dto. But imagination, you have to work with body payload, route params, query params or even header values. Without Dto you have to put your validation code inside each controller's methods to handle the request. With Class Validation and Class Transformer, you can use decorator to do that. Your mission is defining your Dto Classes and add the validation annotations for each property. You can find out the details here How to validate request input in nestjs and How to use pipe in nestjs

Upvotes: 1

Luan Henning
Luan Henning

Reputation: 87

TLDR

The answer to your question is yes, you could use them for shape if you wanted to, but it might be unnecessary in some situations.


DTOs are a great solution for when you need to enforce some shape on your data(specially on the nestjs ecosystem where it interacts a lot with class-validator) or transform it somehow. Examples of that would be when you're recieving data from your client or from another service. In this case the DTOs are the way to go for setting contracts.

However when you're sending data for example, between two layers of the same application -- for instance between your controller and your usecase or between your usecase and your repository -- you might want to use an interface there since you know your data is comming in the correct format in this scenarios.

One key difference to understand is that the interface serves as a development tool, it keeps you for making mistakes like passing an object lacking a certain required property between two classes, while the DTO affects the application itself, it's an object that's going to be instantiated at runtime and might be used for validation and data transformation purposes, that`s the idea, but of course it also has the capacities of an interface.

There might be exceptions to this rule of thumb depending on the architecture you're going for. For example, on a project I'm working on it's very common to have the contract between domain and presentation layers equal to the contract between frontend and the API. On this project, to avoid having to create two similar contracts, I`ve chosen to use a DTO to set the contract between the presentation and domain layers. Now in most cases I just extend the DTO when setting the contracts between API and clients.

Upvotes: 7

Pratik Kulkarni
Pratik Kulkarni

Reputation: 31

In the nestjs-query packages there are two types of DTOs referenced. Read DTO - The DTO returned from queries and certain mutations, the read DTO does not typically define validation and is used as the basis for querying and filtering. Input DTOs - The DTO used when creating or updating records.

Upvotes: 3

OleksiiGa
OleksiiGa

Reputation: 51

DTO has a little bit different mission. This is an additional abstraction for data transfer connection by the network between FE and BE layers of your application and at the same time, DTO gives a description of your data like it doing Interface. But the main mission is a distinct place for data connection and due to this you could implement many helpful things in your DTO layer it could be some transformation for DTO fields values, validation for them, checking with regular expression, etc. So you have a convenient place for attendant changes for data just on early receiving or sending to FE side

Upvotes: 5

Reza Salari
Reza Salari

Reputation: 131

I'm not an expert but I do not understand why we use Dto When we can use the schema model - what is the need for Dto and additional objects

Upvotes: -3

Afaq Shah
Afaq Shah

Reputation: 365

I would like to explain the concept of DTO with the simplest example possible for your better understanding. DTO stands for Data Transfer Object. Now DTO's are used to reduce code duplication. It simply defines a schema which are passed in the parameters of functions to make it easy to get the required data from them. Here is an example of a DTO

export class AuthCredentialsDto {
    email: string;
    password: string;
}

Now if we make a method to check whether the password is correct or not

async password_check(usercredentials: AuthCredentialsDTO)
{
    //Destructuring in JAVASCRIPT
    const {email} = usercredentials;
    //Database Logic to find the email
    return user;
}

Now if we didn't make use of the DTO, then the code would have looked like

async password_check(email: string, password: string)
    {
        //Database Logic to find the email
        return user;
    }

also the point is that this is just one function, in a framework, Multiple function call multiple other functions which requires passing the parameters again and again. Just consider that a function requires 10 parameters. you would have to pass them multiple times. although it is possible to work without a DTO but it is not a development friendly practice. Once you get used to DTO you would love to use them as they save a lot of extra code and effort. Regards

Upvotes: 12

Saniul Ahsan
Saniul Ahsan

Reputation: 61

one thing that dto provides more than interface is. with dto and class validator you can make the validations quickly at request level. But when it comes to interface you cannot add class validator to it. dtos are class in general. that means you have more to do with that than a interface.

Upvotes: 6

user5997038
user5997038

Reputation:

According to the Nestjs docs:

But first (if you use TypeScript), we need to determine the DTO (Data Transfer Object) schema. A DTO is an object that defines how the data will be sent over the network. We could determine the DTO schema by using TypeScript interfaces, or by simple classes. Interestingly, we recommend using classes here. Why? Classes are part of the JavaScript ES6 standard, and therefore they are preserved as real entities in the compiled JavaScript. On the other hand, since TypeScript interfaces are removed during the transpilation, Nest can't refer to them at runtime. This is important because features such as Pipes enable additional possibilities when they have access to the metatype of the variable at runtime.

Upvotes: 38

kosiakMD
kosiakMD

Reputation: 1062

BTW, even despite on DTO is a Java convention it can't solve the problem of Generic fields, e.g.:

@Get(url/${variable})
@Reponse({
   [$variable: string]: $value
})

TS Interfaces can solve this issue only, but you cant describe it in DTO And to show it you will pass some hardcoded example

class ResponseDto {
  @ApiProperty({
    ...
    example: [...]
  })
  [$variable]: SomeTSInterface[]
}

@Reponse({ status: 200, type: ResponseDto })

Upvotes: 0

A. Maitre
A. Maitre

Reputation: 3559

To extend @Victor's answer regarding the DTO concept and its role, I'd like to point that interfaces allow us to set a contract which represents something meaningful in our app. We can then implement and/or extend this contract in other places where needed as well e.g. entity definition for database objects - DAOs, data transfer objects - DTOs, and business models definitions notably.

Also interfaces for DTOs can be shared across a backend and a front-end so that both projects can avoid code duplicate and differences between objects exchanged for ease of development and maintainability.

Upvotes: 5

Preston
Preston

Reputation: 3528

I'm no expert but I'm not using DTO's at all. I really couldn't see a use for them. In each module I have a service, module, entity, and controller.

Upvotes: 31

Related Questions