Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NestJS Do I need DTO's along with entities?

I am creating simple service, that will do simple CRUD. So far I have the entity user:

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column({ name: "first_name" })
  firstName: string;

  @Column({ name: "last_name" })
  lastName: string;

  @Column({ name: "date_of_birth" })
  birthDate: string;
}

Controller:

import { Controller, Get,  Query } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('api/v1/backoffice')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':username')
  findOne(@Query('username') username: string) {
    return this.usersService.findByUsername(username);
  }
}

Service:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}


  findByUsername(username: string): Promise<User | undefined> {
    return this.usersRepository.findOne({ username });
  }
}

With this basic example, I return values from the DB, where some Columns are rename: first_name --> firstName

It does serve my purpose, but on so many places, I see DTO's being used. I know I am not doing correct things, and that I should start using it as well. How would I use the DTO approach with my example?

I am trying to grasp the concept here.

like image 533
Wexoni Avatar asked Mar 02 '23 05:03

Wexoni


1 Answers

First thing, @Carlo Corradini 's comment is right, you should have a look at the class-transformer and class-validator libs, which are also used under the hood in NestJS pipes noticeably and can be nicely combined with TypeORM.

1: As per @Carlo Corradini 's comment and its corresponding links

Now, since your DTO instance is the representation of the data you want to expose to your consumer, you have to instantiate it after you have retrieved your user entity.

  1. create a new user-response.dto.ts file and declare a UserResponseDto class inside it that you'll export. Let's say code would look as follows if you want to expose everything from your previously retrieved User entity

user-response.dto.ts

import { IsNumber, IsString } from 'class-validator';
import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class UserResponseDto {
  @Expose()
  @IsNumber()
  id: number;

  @Expose()
  @IsString()
  username: string;

  @Expose()
  @IsString()
  firstName: string;

  @Expose()
  @IsString()
  lastName: string;

  @Expose()
  @IsString()
  birthDate: string;
}

Here with the @Exclude() at the top of the UserResponseDto, we're telling class-transformer to exclude any field that doesn't have the @Expose() decorator in the DTO file when we'll be instantiating a UserResponseDto from any other object. Then with the @IsString() and @IsNumber(), we're telling class-validator to validate the given fields' types when we'll validate them.

  1. Cast your User entity into a UserResponseDto instance:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}


  async findByUsername(username: string): Promise<User | undefined> {
    const retrievedUser = await this.usersRepository.findOne({ username });

    // instantiate our UserResponseDto from retrievedUser
    const userResponseDto = plainToClass(UserResponseDto, retrievedUser);

    // validate our newly instantiated UserResponseDto
    const errors = await validate(userResponseDto);
    if (errors.length) {
        throw new BadRequestException('Invalid user',
this.modelHelper.modelErrorsToReadable(errors));
    }
    return userResponseDto;
  }
}

2: Another way of achieving it:

You could also use the ClassSerializerInterceptor interceptor from @nestjs/common to automatically cast your returned Entity instances from services into the proper returned type defined in your controller's method. This would mean that you wouldn't even have to bother with using plainToClass within your service and let the job get done by Nest's interceptor itself, with a slight detail as stated by the official docs

Note that we must return an instance of the class. If you return a plain JavaScript object, for example, { user: new UserEntity() }, the object won't be properly serialized.

Code would look as follows:

users.controller.ts

import { ClassSerializerInterceptor, Controller, Get,  Query } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('api/v1/backoffice')
@UseInterceptors(ClassSerializerInterceptor) // <== diff is here
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':username')
  findOne(@Query('username') username: string) {
    return this.usersService.findByUsername(username);
  }
}

users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}


  async findByUsername(username: string): Promise<User | undefined> {
    return this.usersRepository.findOne({ username }); // <== must be an instance of the class, not a plain object
  }
}

Last thoughts: With the latest solution, you could even use the class-transformer's decorators within your User entity file and don't have to declare the DTO file, but you would lose the data validation.

Let me know if it helps or if something is unclear :)

EDIT with incoming payload validation & transformation into proper DTO

You would declare a GetUserByUsernameRequestDto with a username attribute in it, like so: get-user-by-username.request.dto.ts

import { IsString } from 'class-validator';
import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class GetUserByUsernameRequestDto {
  @Expose()
  @IsString()
  @IsNotEmpty()
  username: string;
}

users.controller.ts

import { ClassSerializerInterceptor, Controller, Get,  Query } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('api/v1/backoffice')
@UseInterceptors(ClassSerializerInterceptor) // <== diff is here
@UsePipes( // <= this is where magic happens :)
    new ValidationPipe({
        forbidUnknownValues: true,
        forbidNonWhitelisted: true,
        transform: true
    })
)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':username')
  findOne(@Param('username') getUserByUsernameReqDto: GetUserByUsernameRequestDto) {  
    return this.usersService.findByUsername(getUserByUsernameReqDto.username);
  }
}

Here we're using Nest's pipes concept - @UsePipes() - to get the job done. along with built-in ValidationPipe from Nest as well. You can refer to the docs both from Nest and class-validator themselves to learn more about the options to pass to the ValidationPipe

So this way your incoming params and payload data can be validated before being processed :)

like image 60
A. Maitre Avatar answered Mar 11 '23 05:03

A. Maitre