typescriptnestjsdomain-driven-designclean-architecturemapper

What's the point of creating mappers to convert entity<>DTOs?


I've seen some people use classes called 'mappers' that convert DTOs to entities or entities to DTOs. But why would I do that? What do I gain by doing that while developing my backend?

I would like to better understand why people use it, with an example in Express or NestJS. I still cannot grasp the concept.


Solution

  • I think the point of your question is "why to use DTOs?". Because ssing DTOs you need mappers to transform data between entities and DTOs. But... why is it necessary to have DTOs and add classes and complexity into the code?

    Well, I'll try to answer in a superficial way because there are a lot of lines written about that and to answer perfectly your question maybe you need an entire book.

    So, short answer: To meet layer architecture you need separate your code in layers, and to not overlap layers you can't use the same object to save into DB (entity) and to manage your business logic. That's mean in some point you have to map between an entity -object retrieved from the DB- and a domain object -object that define your program data behaviour-. (Note that you may need to understand hexagonal/layered/clean architecture)

    Using a simple example about this, imagine that you have an APP where you store users. If you have layers you should have something like Service layer where UserService contains the business logic and another layer to treat the entities, something like UserEntity, tightly coupled to DB (and it is necessarily coupled to DB, there is no problem). And also another class coupled to DB called UserRepository.

    So your code may be like this (pseudocode, not compiled):

    class UserService {
        constructor(@Inject() private readonly repository: UserRepository)
    
        getUser() {
            const user: UserEntity = this.repository.getUser()
            // check whatever from the user, do your logic...
            return user
        }
    }
    

    Note that here, you are getting Entity into business logic, and what does it mean?.

    First of all, assuming your Entity has id, password, and/or many other sensitive fields... you are returning data that may you don't want. You are getting the object as exists into DB (i.e. as needed by a third party). Now, your business logic is based on an external tool, that's means the DB decides how your objects are. Clearly that's a red flag.

    Another point is that in top of your class you can see something like:

    import { UserEntity } from '../../../some/path/INFRASTRUCTURE/entities'
    

    In other words: you are coupling layers.

    So, how can you avoid this? Using DTOs to expose only values you want:

    class UserDto {
        constructor(public name: String)
    }
    

    Now you can have something like this:

    class UserService {
        constructor(@Inject() private readonly repository: UserRepository)
    
        getUser() {
            const user: UserEntity = this.repository.getUser()
            return new UserDto(user.name, user.surname)
        }
    }
    

    I.e. the line return new UserDto(user.name) is a mapper between entity and DTO. So thinking bigger you can extract a class to have that behaviour.

    But also, for me, there is an even more important point about that. You can stop here and think mapper is needed to translate the data as exists into DB and data you want to expose. Is ok if you are starting into architecture world, also I've read a lot of tutorials explaining DTOs using an example like this last of mine. So as a first step I consider it is ok.

    But for me is incomplete. The example still import the Entity into your service. So the point to have a mapper is also to extract the entity from the business logic and to get the desired value "prepared" for your logic.

    So, if your layers are Service and Infrastructure you can place classes in their proper packages/modules:

    And your classes will be placed:

    Now your repository will get an Entity and will call your mapper to return a DTO. That means, the repository is coupled to the DB, but the returned object is based in your domain... and where have the "magic" done? Into the mapper:

    import { UserDto } from '../mapper/user.mapper'
    
    class UserRepository {
        // ...
    
        getUser() {
            const user: UserEntity = this.db.getUser()
            const userDto: UserDto = this.mapper.toUserDto(user)
            return userDto
        }
    }
    

    You receive an Entity and return and object based in your needs.

    import { UserEntity } from '../entity/user.entity'
    import { UserDto } from '../../domain/user.dto'
    
    class UserMapper {
    
        toUserDto(user: UserEntity) {
            return new UserDto(user.name, user.surname)
        }
    }
    

    So now, your business logic don't depend for any entity and this method does not return data like id or password. Now, your data into business layer is strictly defined by your domain, not by your DB.

    import { UserDto } from '../../domain/user.dto'
    
    class UserService {
        getUser() {
            const user: UserDto = this.repository.getUser()
            return user
        }
    }
    

    So using mappers you have decoupled your code, now your business, domain and infrastructure layers are not tightly coupled.

    PS: Even with this explanation superficial to explain why are mapped used, for me, as I understand DTOs, this example is true because is the value returned by service to an imaginary controller and to the user, by the way, for me, objects into business logic are VOs (value objects with some logic if needed) and DTOs are exposed to the client. That means the explanation is the same but I should use DTOs between service and controller layer and, between repository and services I use VOs. By the way the reason to do that and why it is used is absolutely the same as exposed.

    PS2: To accomplish absolutely decoupling between layers, using NestJS you can inject an interface defining the implementation (in other languages and frameworks as Spring Boot that's trivial but JS doesn't have interfaces at runtime so it is not trivial here). Into service inject a repository interface which is placed into domain layer. Yes, repository interface into domain, that's because you decide how to model your data, your domain decide that it wants an object with X attributes from the DB. And that is done into a mapper between repository layer and application/service layer.

    PS3: Not explained but the idea to convert Dto to Entity (the reverse way) is obvious I think... you are working with model data and your DB needs another format. Maybe your domain and business needs value called age but DB stores birth_year. So your mapper will also translate that.