Exploring NestJs: The Importance of DTOs and Entities

In my project, I'm currently experimenting with utilizing DTOs and Entities in a clever manner. However, I find it more challenging than expected as I develop a backend system for inventory management using NestJs and TypeOrm.

When my client sends me a set of data via a POST request, the structure looks something like this:

{
  "length": 25,
  "quantity": 100,
  "connector_A": {
    "id": "9244e41c-9da7-45b4-a1e4-4498bb9de6de"
  },
  "connector_B": {
    "id": "48426cf0-de41-499b-9c02-94c224392448"
  },
  "category": {
    "id": "f961d67f-aea0-48a3-b298-b2f78be18f1f"
  }
}

My controller's responsibility is to validate the incoming data using a custom ValidationPipe:

@Post()
  @UsePipes(new ValidationPipe())
  create(@Body() data: CableDto) {
    return this.cablesService.create(data);
}

In accordance with best practices, it is recommended to convert raw data into DTOs before casting them into TypeOrm entities during data insertion.

While I understand and agree with this approach, I must admit that I find it quite complex, especially when dealing with table relations and prefix nouns.

Here is an example of my cable Entity:

@Entity('t_cable')
export class Cable {

  @PrimaryGeneratedColumn('uuid')
  CAB_Id: string;

  @Column({
    type: "double"
  })
  CAB_Length: number;

  @Column({
    type: "int"
  })
  CAB_Quantity: number;

  @Column()
  CON_Id_A: string

  @Column()
  CON_Id_B: string
  
  @Column()
  CAT_Id: string

  @ManyToOne(type => Connector, connector => connector.CON_Id_A)
  @JoinColumn({ name: "CON_Id_A" })
  CON_A: Connector;

  @ManyToOne(type => Connector, connector => connector.CON_Id_B)
  @JoinColumn({ name: "CON_Id_B" })
  CON_B: Connector;

  @ManyToOne(type => Category, category => category.CAB_CAT_Id)
  @JoinColumn({ name: "CAT_Id" })
  CAT: Category;

}

And here is the corresponding DTO for cable interactions:

export class CableDto {

  id: string;

  @IsOptional()
  @IsPositive()
  @Max(1000)
  length: number;
  quantity: number;

  connector_A: ConnectorDto;
  connector_B: ConnectorDto;
  category: CategoryDto

  public static from(dto: Partial<CableDto>) {
    const it = new CableDto();
    it.id = dto.id;
    it.length = dto.length;
    it.quantity = dto.quantity;
    it.connector_A = dto.connector_A
    it.connector_B = dto.connector_B
    it.category = dto.category
    return it;
  }

  public static fromEntity(entity: Cable) {
    return this.from({
      id: entity.CAB_Id,
      length: entity.CAB_Length,
      quantity: entity.CAB_Quantity,
      connector_A: ConnectorDto.fromEntity(entity.CON_A),
      connector_B: ConnectorDto.fromEntity(entity.CON_B),
       category: CategoryDto.fromEntity(entity.CAT)
    });
  }

  public static toEntity(dto: Partial<CableDto>) {
    const it = new Cable();
    if (dto.hasOwnProperty('length')) {
      it.CAB_Length = dto.length;
    }
    if (dto.hasOwnProperty('quantity')) {
      it.CAB_Quantity = dto.quantity;
    }
    if (dto.hasOwnProperty('connector_A')) {
      it.CON_Id_A = dto.connector_A.id;
    }
    if (dto.hasOwnProperty('connector_B')) {
      it.CON_Id_B = dto.connector_B.id;
    }
    if (dto.hasOwnProperty('category')) {
      it.CAT_Id = dto.category.id;
    }
    return it;
  }
}

While these conversion methods between DTOs and entities may seem cumbersome, I believe there might be a simpler or more efficient solution out there.

Do you have any suggestions or insights on how to streamline this process better?

Thank you!

Answer №1

To handle all sorts of type conversions, such as DTO to entity or entity to DTO, I've created a convenient library called metamorphosis-nestjs. This library aims to simplify the process of converting objects in NestJS by providing an injectable conversion service for all your converters, which are registered early on into the conversion service similar to how it's done in Spring Framework for Java applications.

For integration with TypeORM:

  1. npm install --save @fabio.formosa/metamorphosis-nest
    
  2. import { MetamorphosisNestModule } from '@fabio.formosa/metamorphosis-nest';
    
    @Module({
      imports: [MetamorphosisModule.register()],
      ...
    }
    export class MyApp{ }
    
  3. import { Convert, Converter } from '@fabio.formosa/metamorphosis';
    
    @Injectable()
    @Convert(CableDto, Cable)
    export default class CableDtoToCableConverter implements Converter<CableDto, Promise<Cable>> {
    
    constructor(private readonly connection: Connection){}
    
    public async convert(source: CableDto): Promise<Cable> {
      const cableRepository: Repository<Cable> = this.connection.getRepository(Cable);
      const target: Product | undefined = await cableRepository.findOne(source.id);
      if(!target)
        throw new Error(`not found any cable by id ${source.id}`);
      target.CAB_Length = source.length;
      target.CAB_Quantity = source.quantity;
      ... and so on ...
      return target;
    }
    

}

Once configured, you can now easily inject and utilize the conversionService as needed:

 const cable = <Cable> await this.convertionService.convert(cableDto, Cable);

The converter from Cable to CableDto follows a simpler structure.

Refer to the README for detailed examples and advantages of using metamorphosis-nestjs.

Answer №2

One effective way to handle conversions between Entity and DTO is by using Automapper.

I have utilized Automapper in various projects with excellent outcomes, particularly in projects containing numerous DTOs and Entities.

For instance, you can implement a solution like the following:

    export class InitializeMapper {
        mapper: AutoMapper;
    
        constructor() {
            this.mapper = new AutoMapper();
    
            this.mapper.createMap(CableDto, CableEntitt)
                .forMember(dst => dst.length, mapFrom(s => s.length))
                .forMember(dst => dst.quantity, mapFrom(s => s.quantity))
                .forMember(dst => dst.connector_A, mapFrom(s => s.connector_A.id))
                .forMember(dst => dst.connector_B, mapFrom(s => s.connector_B.id))
                .forMember(dst => dst.connector_C, mapFrom(s => s.connector_C.id));
            });
        }
        
        map(a: any, b: any) {
            return this.mapper.map(a, b);
        }
    
        mapArray(a: Array<any>, b: any) {
            return this.mapper.mapArray(a, b);
        }
    }

This enables easy reuse of the same mapping logic across different parts of your project.

Best regards

Answer №3

If you're looking for some boilerplate code to review and gather inspiration from, there are some great examples available. These examples provide clear and straightforward solutions for working with DTOs and entities.

For instance, here's a helpful example of DTO conversion:
https://github.com/NarHakobyan/awesome-nest-boilerplate/blob/main/src/modules/user/dtos/user.dto.ts
With just a bit of effort, you can adapt this code to suit your needs.

Additionally, there's another example demonstrating the use of a Mapper. In this case, it is used to convert entities to DTOs, but the same principle can be applied in reverse:
Mapper Usage:
https://github.com/nairi-abgaryan/analyzer/blob/master/src/modules/user/user.service.ts#L48

You can find the Mapper implementation here:
https://github.com/nairi-abgaryan/analyzer/blob/master/src/providers/mapper.service.ts

Answer №4

If you are looking to implement a mapping function, consider the following approach:

function MapObjectToDTO(cable: Cable): CableDto {
  const cableDto = new CableDto();

  for (const key in cable) {
    if (cable[key] instanceof Object) {
      cableDto[key] = MapObjectToDTO(cable[key]);
    } else {
      cableDto[key] = cable[key];
    }
  }

  return dto;
}

To make this more generic and reusable, you can take advantage of generics like so:

function MapObjectToDTO<T>(obj: Object): T

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to create a T-shaped hitbox for an umbrella?

I've recently dived into learning Phaser 3.60, but I've hit a roadblock. I'm struggling to set up the hitbox for my raindrop and umbrella interaction. What I'd love to achieve is having raindrops fall from the top down and when they tou ...

Why am I encountering numerous errors while attempting to install Juice Shop?

My attempt to install the juice shop app from GitHub resulted in 63 errors showing up after running the command npm install. [riki@anarchy]: ~/juiceShop/juice-shop>$ npm install (Various warnings and engine compatibility issues) Multiple vulnerabilit ...

TypeScript - Indexable Type

Here is an explanation of some interesting syntax examples: interface StringArray { [index: number]: string; } This code snippet defines a type called StringArray, specifying that when the array is indexed with a number, it will return a string. For e ...

Creating a Typescript interface that includes keys from another interface

interface A{ a: string; b: string; c: string; // potentially more properties } interface B{ [K in keyof A]: Boolean; } What could be the issue with this code? My goal is to generate a similar structure programmatically: interface B{ ...

Generating a date without including the time

I recently started working with React (Typescript) and I am trying to display a date from the database without including the time. Here is my Interface: interface Games { g_Id: number; g_Title: string; g_Genre: string; g_Plattform: string; g_ReleaseDate: ...

What is the best approach to handling an undefined quantity of input FormControls within Angular?

I have a unique task in my Angular application where I need to collect an unspecified number of entries, such as names, into a list. My goal is to convert this list of names into an array. To facilitate this process, I would like to offer users the abilit ...

Errors have been encountered in the Angular app when attempting to add FormControl based on the data retrieved from the backend

This specific page is a part of my Angular application. Within the ngOnInit method, I make two API calls to retrieve necessary data and iterate through it using forEach method to construct a reactive form. However, I am facing one of two different errors ...

Find out if all attributes of the object are identical

I am trying to create the boolean variable hasMultipleCoverageLines in order to determine whether there are multiple unique values for coverageLineName within the coverageLines items. Is there a more efficient way to write this logic without explicitly c ...

Signatures overburdened, types united, and the call error of 'No overload matches'

Consider a TypeScript function that takes either a string or a Promise<string> as input and returns an answer of the same type. Here's an example: function trim(textOrPromise) { if (textOrPromise.then) { return textOrPromise.then(val ...

What is the best way to format a string into a specific pattern within an Angular application

In my Angular component, I have 4 fields: customerName, startDate, and startTime. Additionally, I have a fourth field that is a textarea where the user can see the message that will be sent via email or SMS. Within my component, I have defined a string as ...

Creating a function that utilizes a default argument derived from a separate argument

Consider this JavaScript function: function foo({ a, b, c = a + b }) { return c * 2; } When attempting to add type annotations in TypeScript like so: function foo({ a, b, c = a + b }: { a?: number, b?: number, c: number }): number { return c * 2; } ...

The entry for package "ts-retry" could not be resolved due to possible errors in the main/module/exports specified in its package.json file

I encountered an error while attempting to run my React application using Vite. The issue arises from a package I am utilizing from a private npm registry (@ats/graphql), which has a dependency on the package ts-retry. Any assistance in resolving this pro ...

Filtering nested arrays in Javascript involves iterating through each nested

I have a nested array inside an array of objects in my Angular app that I'm attempting to filter. Here is a snippet of the component code: var teams = [ { name: 'Team1', members: [{ name: 'm1' }, { name: 'm2' }, { name ...

The 'data-intro' property cannot be bound to the button element as it is not recognized as a valid property

I've been using the intro.js library in Angular 8 and so far everything has been working smoothly. However, I've hit a roadblock on this particular step. I'm struggling to bind a value in the data-intro attribute of this button tag. The text ...

Angular/NestJS user roles and authentication through JWT tokens

I am encountering difficulties in retrieving the user's role from the JWT token. It seems to be functioning properly for the ID but not for the role. Here is my guard: if (this.jwtService.isTokenExpired() || !this.authService.isAuthenticated()) { ...

Modeling a potentially empty array in Typescript can be achieved by implementing specific interface definitions

Here is the current situation: type A = { b: string, c: number } I have an object that I will receive from an API, which could be either A[] or [] As of now, when I attempt to use it, const apiData: A[] || [] const b = apiData[0].a // I expected this to ...

What is the best way to combine individual function declarations in TypeScript?

In my project, I am currently developing a TypeScript version of the async library, specifically focusing on creating an *-as-promised version. To achieve this, I am utilizing the types provided by @types/async. One issue I have encountered is that in the ...

Guide on linking enum values with types in TypeScript

My enum type is structured as follows: export enum API_TYPE { INDEX = "index_api", CREATE = "create_api", SHOW = "show_api", UPDATE = "update_api", DELETE = "destroy_api" }; Presently, I have a f ...

Dropdown box not displaying any choices

I developed a basic reusable component in the following way: Typescript (TS) import {Component, Input, OnInit} from '@angular/core'; import {FormControl} from '@angular/forms'; @Component({ selector: 'app-select', templa ...

Move forward state using navigateByUrl in Angular

I am looking to send data when changing the view using navigateByUrl like this: this.router.navigateByUrl(url, {state: {hello: "world"}}); Once in the next view, my goal is to simply log the hello attribute like so: constructor(public router: Router) { ...