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.
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
.
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.
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
entityuser-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.
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;
}
}
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 :)
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 :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With