Handling the mapping of one object to another can be a rather annoying and repetitive task in all applications with a pretty large amount of code that looks almost the same, and the same goes for Javascript and Typescript applications. So, for example, managing the DTOs that the server will return to the client after it executes a database query has to be written separately for every mapping. The solution we found is Automapper, a Typescript implementation of Automapper from the .NET universe. It is best described on its official website as:

“AutoMapper (TypeScript) is an object-object mapper by convention. When two objects’ models are conventionally matching, AutoMapper can map the two objects with almost zero mapping configuration."

So this is exactly what we used it for, mapping the entire objects we got from the database to DTOs we needed while respecting the structure and relations between entities with the minimal amount of code. That way, our controllers stay concise, and the same convention is enforced for all mappings.

Installation

To add automapper to the NestJS project, use the following commands:

npm install @automapper/core @automapper/nestjs

Those two are the main packages, but we will also need the classes package for some more decorators, and it is done with the following command:

npm install @automapper/classes

There are a few more packages that can be additionally used for some purposes, but these three are all we need in our case.

How to use it?

Since we are in NestJS, the first thing we need to do is make an Automapper module.

import { classes } from '@automapper/classes';
import { AutomapperModule } from '@automapper/nestjs';
import { Module } from '@nestjs/common';

@Module({
    imports: [
        AutomapperModule.forRoot({
            strategyInitializer: classes()
        })
    ] })

export class AutoMapperModule {}

The most important part of the module setup is to define what strategy will be used for mapping. We decided on a class strategy, which is used for mapping objects between classes. It allows us to define mappings between properties of different classes and is useful when we want to map data from one class to another.

To show the abilities of Automapper, we first need an example app. As with almost any other application, ours will also have users. We will store all data in a relational database, in this example, it will be postgres and use typeorm for database manipulation. So we will define the initial user entity, but before that, we will create one abstract class that will represent some basic properties every entity has and will be extended by other entities.

import { PrimaryGeneratedColumn, UpdateDateColumn, CreateDateColumn,
DeleteDateColumn } from 'typeorm';
import { AutoMap } from '@automapper/classes';

export abstract class BaseEntity {
    @AutoMap()
    @PrimaryGeneratedColumn()
    id: number;

    @AutoMap()
    @CreateDateColumn({ type: 'timestamptz' })
    createdAt: Date;

    @AutoMap()
    @UpdateDateColumn({ type: 'timestamptz' })
    updatedAt?: Date;

    @AutoMap()
    @DeleteDateColumn({ type: 'timestamptz' })
    deletedAt?: Date;
}

Now let’s define the user class.

import { Entity } from 'typeorm';
import { AutoMap } from '@automapper/classes';

@Entity()
export class User extends BaseEntity {
    @AutoMap()
    @TextColumn({ nullable: false })
    firstName: string;

    @AutoMap()
    @TextColumn({ nullable: false })
    lastName: string;

    @AutoMap()
    @TextColumn({ nullable: false })
    country: string;

    @AutoMap()
    @TextColumn({ nullable: false })
    city: string;

    @AutoMap()
    @TextColumn({ nullable: false })
    address: string;

    @AutoMap()
    @DateColumn({ nullable: false })
    birthDate: Date;

    @AutoMap(() => String)
    @EnumColumn({ enum: UserSex, nullable: false })
    sex: UserSex;
}

As you may have already noticed, every property has an additional decorator above and it is AutoMap(). That is a small overhead in code but it must be added to every property so it could be mapped. For the basic data types in Typescript, there is no need to add additional parameters, but there is an example where that is not enough. So for example, if there is an enum type, in this case it is the sex of the user, we need to explicitly map that enum to string and it is simply done by mapping the enum type to string as a decorator parameter.

Now that we defined our user, let’s see how we will use Automapper to return the needed DTOs from the server. Let’s say we want a route in the user controller that will get a user id as a parameter and return the user’s first name, last name and date of birth. If we don’t use automapper, there are two options: add a select clause to the database query or retrieve the entire user object from the database and then map it manually. Although it seems that the first solution would work just fine, as the application grows, writing distinct functions just because of the different select clauses for every query and not reusing already existing ones would become unsustainable. So that’s why we use Automapper.

This is how it would be used in the user controller in the route described above. To make automapper work at all, we use the power of dependency injection in NestJS and inject it in the controller constructor.

import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';

@Controller('/users)
export class UserController {
    constructor(
        private readonly userService: UserService,
        @InjectMapper() private readonly automapper: Mapper
    ) {}

Once the mapper is injected, we simply use the map function which receives 3 parameters:

  1. the object that will be mapped
  2. its current class
  3. the class it will be mapped to
const user: User = await this.userService.findOneById(userId);
const message = 'User data fetched successfully.';
const payload = this.automapper.map(user, User, GetUserPersonalInfoResponseDto);

return createHttpResponse(HttpStatus.OK, message, payload);

The class the user will be mapped to looks like this.

import { AutoMap } from '@automapper/classes';

export class GetUserPersonalInfoResponseDto {
    @AutoMap()
    firstName: string;

    @AutoMap()
    lastName: string;

    @AutoMap()
    birthDate: Date;
}

Again, the properties need to have the @AutoMap decorator, but any property can be added or taken away without the need for additional code.

The last step in order to make this work is to make a profile. Profile is a class where we specify all the mappings used, and it needs to be specified as the provider in the module where it is used. In this case, it looks like this:

export class UserProfile extends AutomapperProfile {
    constructor(@InjectMapper() mapper: Mapper) {
        super(mapper);
    }

    get profile(): MappingProfile {
        return (mapper) => {
            createMap(mapper, Patient, GetUserPersonalInfoResponseDto);
        };
    }
}

The createMap method is added for every mapping used with the same parameters as the map method in the controller.

And that’s it. Our automapper works, and we can manipulate DTOs rather easily.

Advanced usage

After the initial and simplest example, we will discuss some more complex cases when using Automapper.

Mapping of the arrays

The initial example showed the case in which a single object was mapped from one class to another. However, there might be a case in which you have an array of objects and want to map all of them. We will use the same example from above and the only change that needs to be made in the code regarding the mapping is to call the mapArray function instead of the map function.

const users: User[] = await this.userService.findAllUsers();
const payload = this.automapper.mapArray(users, User,
GetUserPersonalInfoResponseDto);

Handling entity relations

In reality, we will rarely have a situation where an entity has no other related entities, and in many cases, we need to handle those relations when mapping complex objects to DTOs. It would be best to show it as an example. Let’s say we need to make a banking app that will keep records of all the users bank accounts. Also, for legal purposes, the bank needs to store records of users personal documents, in other words, they store users ID card data. In typeorm we would model it like this:

import { AutoMap } from '@automapper/classes';
import { Entity, JoinColumn, ManyToOne, OneToMany, OneToOne } from 'typeorm';

@Entity()
export class Account extends BaseEntity {
    @AutoMap()
    @TextColumn({ nullable: false })
    accountNumber: string;

    @AutoMap()
    @IntegerColumn({ nullable: false })
    amount: number;
    
    @AutoMap(() => User)
    @ManyToOne(() => User, (user) => user.accounts, {
        nullable: false
    })
    user: User;
}
import { AutoMap } from '@automapper/classes';
import { Entity } from 'typeorm';

@Entity()
export class IdentityCard extends BaseEntity {
    @AutoMap()
    @TextColumn({ nullable: false })
    cardNumber: string;

    @AutoMap()
    @DateColumn({ nullable: false })
    expirationDate: Date;
}

Some additional code needs to be added to the User model as well.

import { OneToMany, OneToOne } from 'typeorm';

@AutoMap(() => [Account])
@OneToMany(() => Account, (account) => account.user, {
    cascade: ['insert']
})
accounts: Account[];

@AutoMap(() => IdentityCard)
@OneToOne(() => IdentityCard, { nullable: false, cascade: ['insert'] })
@JoinColumn()
identityCard: IdentityCard;

Again, as in previous cases, the @Automap decorator must be added to all properties, but here some new concepts can be observed. In the case of the one-to-one relation, the object is simply mapped by adding the arrow function and specifying the class name. In the one-to-many relation, the class name must be specified inside the square brackets. Some may think the brackets would come after the class name as they do in Typescript when specifying the array of objects, but that is not the right way to do it here.

Now that we have all the entities we need, we can define the DTO we want to be returned, which will consist of user id, accounts info and identity card info.

export class GetPatientIdAndAccountsAccount {
    @AutoMap()
    accountNumber: string;

    @AutoMap()
    amount: number;
}

export class GetPatientIdAndAccountsIdentityCard {
    @AutoMap()
    cardNumber: string;
}
    
export class GetPatientIdAndAccountsResponseDto {
    @ApiProperty()
    @AutoMap()
    id: number;

    @ApiProperty({ type: [GetPatientIdAndAccountsAccount] })
    @AutoMap(() => [GetPatientIdAndAccountsAccount])
    accounts?: GetPatientIdAndAccountsAccount[];

    @ApiProperty({ type: GetPatientIdAndAccountsIdentityCard })
    @AutoMap(() => GetPatientIdAndAccountsIdentityCard)
    identityCard: GetPatientIdAndAccountsIdentityCard;
}

For every mapping, in this case all 3 of them, the createMap method should be added to the appropriate profile for each mapping, and that is it. The DTOs can now return whatever properties they need while preserving the structure and relations between the data models.

Custom mapping

In some cases we might want to have a custom mapping of some property. For example, we might need a DTO with the user’s full name property and our model only has the first name and the last name property. The way it can be done with Automapper is to use the forMember method inside the createMap function in the profile.

export class UserProfile extends AutomapperProfile {
    constructor(@InjectMapper() mapper: Mapper) {
        super(mapper);
    }

    get profile(): MappingProfile {
        return (mapper) => {
            createMap(mapper, Patient, GetUserPersonalInfoResponseDto);
            createMap(mapper, User, GetUserFullNameResponseDto)
            .forMember(
                (destination) => destination.fullName,
                mapFrom((source) => `${source.firstName} ${source.lastName}`),
            );
        };
    }
}

The forMember method takes 2 arguments, the first one of which is the lambda function of the destination property you want to map. In this case, it is the full name in the response DTO. The second argument is a function that defines how to map the source property to the destination property. Here it is the formatted string where the first and last name are concatenated. There can also be one additional parameter, and that is the precondition function, which can be called if there is a need to do a pre-check against some condition before executing the mapping.

Conclusion

Using Automapper helps us with object mapping with the minimal amount of code needed. If we need to change something in the logic, it is straightforward, and we instantly know where to look or what to change without spending too much time on debugging. It is pretty easy to work with and will significantly speed up your development.