1. Class Validator์ DTO ์ฌ์ฉ
1.1 Class Validator ํน์ฑ
- TS Decorator๋ฅผ ์ฌ์ฉํด์ ํด๋์ค๋ฅผ ๊ฒ์ฆํ๋ค (Validate)
- ๋๊ธฐ (Synchronous), ๋น๋๊ธฐ (Asynchronous) ๋ฐฉ์ ๋ชจ๋๋ฅผ ์ง์ํ๋ค.
- Class Validator ์์ฒด์ ์ผ๋ก ์ ๊ณตํด์ฃผ๋ Validator๋ค์ ์ฌ์ฉ ํ ์ ์๋ค.
- ์ปค์คํ Validator๋ฅผ ์ฝ๊ฒ ๋ง๋ค ์ ์๋ค.
- ์ปค์คํ ์๋ฌ ๋ฉ์ธ์ง๋ฅผ ๋ฐํ ํ ์ ์๋ค.
1.2 Class Validator ์ค์น ๋ฐ ์ฌ์ฉ๋ฒ
(1) Class Validator ์ค์น
1yarn add class-validator
(2) Class Validator ์ฌ์ฉ๋ฒ
1class User {2@IsNotEmpty()3name: string45@IsEmail()6email: string7}
์ด๋ค Validator๋ ๊ฒ์ฆํ๊ณ ์ถ์ ํ๋กํผํฐ์ Class Validator์์ ์ ๊ณตํ๋ Decorator๋ก ๋ถ์ฌ์ฃผ๋ฉด ๋๋ค.
1const user = new User()23user.name = ''4user.email = 'invalid-email'56validate(user).then(errors => {7// ์ฌ๊ธฐ์ ์๋ฌ ๋ฐํ8})
validate()๋ก ๊ฐ์ฒด๋ฅผ ๊ฒ์ฆํ์๋, Class Validator์ ๋ถํฉํ์ง ์์ ๊ฐ์ด ์
๋ ฅ๋๋ฉด, ํด๋น๋๋ ์๋ฌ๊ฐ ๋ฐํ๋๋ค.
1.3 DTO (Data Transfer Object)
class-validator๋ก Body์ ํ๋กํผํฐ ๊ฐ๋ค์ ํ๋์ ํด๋์ค๋ก ๋ฌถ์ด์ ๊ด๋ฆฌํ ์ ์๋ค.
- ๊ทธ๋ฆฌ๊ณ ์ด๋ ๊ฒ ๋ฌถ์ ํํ์ ํด๋์ค๋ฅผ DTO๋ผ๊ณ ๋ถ๋ฅธ๋ค.
- DTO(Data Transfer Object; ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ ๊ฐ์ฒด)
- ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ์๋ฒ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ก๋ฐ์ผ๋ฉด,
- ๊ทธ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์์ ํจ์จ์ ์ผ๋ก ์ฌ์ฉํ ์ ์๊ฒ ๊ด๋ฆฌํด์ฃผ๋ ๊ฐ์ฒด๋ผ๋ ๋ป
posts/dto/create-post.dto.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { IsString } from 'class-validator'23export class CreatePostDto {4@IsString()5title: string67@IsString()8content: string9}
- cf. DTO๋ API๋ 1:1 ๋งตํ์ด ์๋ ๊ฒฝ์ฐ๋ ์๋ค.
- ๊ทธ๋์ CreatePostDTO๋ฅผ ๋ค๋ฅธ API์์๋ ์ฐ๋ ๊ฒฝ์ฐ๊ฐ ์๊ธฐ์ ์กฐ๊ธ ๋ ์ผ๋ฐํ๋ ์ด๋ฆ์ผ๋ก ์ง๋๋ค.
posts ์ปจํธ๋กค๋ฌ์ DTO๋ฅผ ์ ์ฉ์ํจ๋ค.
1// posts.controller.ts ์๋ต2@Post()3@UseGuards(AccessTokenGuard)4postPosts(5@User('id') userId: number,6@Body() body: CreatePostDto,7// @Body('title') title: string,8// @Body('content') content: string,9// ๊ธฐ๋ณธ๊ฐ์ true๋ก ์ค์ ํ๋ ํ์ดํ10// @Body('isPublic', new DefaultValuePipe(true)) isPublic: boolean,11) {12return this.postsService.createPost(userId, body)13}
posts ์๋น์ค์๋ DTO๋ฅผ ์ ์ฉ์์ผ์ค๋ค.
1// posts.service.ts ์๋ต2/**3* 1) create : ์ ์ฅํ ๊ฐ์ฒด๋ฅผ ์์ฑ4* 2) save : ๊ฐ์ฒด๋ฅผ ์ ์ฅ (create ๋ฉ์๋์์ ์์ฑํ ๊ฐ์ฒด๋ก)5*/6async createPost(authorId: number, postDto: CreatePostDto) {7const post = this.postsRepository.create({8author: {9id: authorId,10},11...postDto,12likeCount: 0,13commentCount: 0,14})15const newPost = await this.postsRepository.save(post)16return newPost17}
main.ts์์ ์ฑ ์ ์ฒด์ ํ์ดํ๋ฅผ ์ ์ญ์ ์ผ๋ก ์ค์ ํ๋ค.
1import { NestFactory } from '@nestjs/core'2import { AppModule } from './app.module'3import { ValidationPipe } from '@nestjs/common'45async function bootstrap() {6const app = await NestFactory.create(AppModule)7app.useGlobalPipes(new ValidationPipe()) // ์ถ๊ฐ8await app.listen(3000)9}10bootstrap()
- ํด๋น ์ฑ์ ์ ๋ฐ์ ์ผ๋ก ์ ์ฉํ ํ์ดํ๋ฅผ ๋ฃ์ด์ฃผ๋ ๊ฒ์ธ๋ฐ,
- ValidationPipe๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ๋ชจ๋ class-validator๋ค์ ์ ๋ ธํ ์ด์ ๋ค์ด
- ๋ฐ๋ก ์ปจํธ๋กค๋ฌ์๋ค๊ฐ validation ์ ์ฉํ๊ฑฐ๋, ๋ชจ๋์๋ค validation ๋ชจ๋์ ์ถ๊ฐํ์ง ์์๋
- ์ฑ ์ ๋ฐ์ ์ผ๋ก validation์ ์ฌ์ฉํ ์ ์๊ฒ ๋๋ค.
2. ๊ธฐ๋ณธ ์ ๊ณต Class Validator
- https://github.com/typestack/class-validator?tab=readme-ov-file#validation-decorators
- Common validation decorators, Type validation decorators
- 2๊ฐ๋ฅผ ์์ฃผ ์ฌ์ฉํ๋๋ฐ, ๋ค๋ฅธ Validator๋ ์ฐพ์๋ณด๊ณ ์ฌ์ฉํ๋ฉด ๋๋ค.
| ๋ฐ์ฝ๋ ์ดํฐ | ์ค๋ช |
|---|---|
| ๊ณตํต Validator | |
| @IsDefined(value: any) | ๊ฐ์ด ์ ์๋์๋์ง ํ์ธ(!== undefined, !== null). ์ด ๋ฐ์ฝ๋ ์ดํฐ๋ skipMissingProperties ์ต์ ์ ๋ฌด์ํ๋ ์ ์ผํ ๋ฐ์ฝ๋ ์ดํฐ |
| @IsOptional | ์ฃผ์ด์ง ๊ฐ์ด ๋น์ด ์๋์ง(=== null, === undefined ์์) ํ์ธ, ๊ทธ๋ ๋ค๋ฉด ํ๋กํผํฐ์ ๋ชจ๋ ์ ํจ์ฑ ๊ฒ์ฌ๊ธฐ๋ฅผ ๋ฌด์ |
| @Equals | ๊ฐ์ด ๊ฐ์(โ===โ) ๋น๊ต์ธ์ง ํ์ธ |
| @NotEquals | ๊ฐ์ด ๊ฐ์ง ์์(โ!==โ) ๋น๊ต์ธ์ง ํ์ธ |
| @IsEmpty | ์ฃผ์ด์ง ๊ฐ์ด ๋น์ด ์๋์ง(=== โโ, === null, === undefined) ํ์ธ |
| @IsNotEmpty | ์ฃผ์ด์ง ๊ฐ์ด ๋น์ด ์์ง ์์์ง(!== โ, !== null, !== undefined) ํ์ธ |
| @IsIn(values: any[]) | ๊ฐ์ด ํ์ฉ๋ ๊ฐ์ ๋ฐฐ์ด์ ์๋์ง ํ์ธ |
| @IsNotIn(values: any[]) | ํ์ฉ๋์ง ์๋ ๊ฐ์ ๋ฐฐ์ด์ ๊ฐ์ด ์๋์ง ํ์ธ |
| ํ์ Validator | |
| @Isboolean | ๊ฐ์ด boolean์ธ์ง ํ์ธ |
| @IsDate | ๊ฐ์ด ๋ ์ง์ธ์ง ํ์ธ |
| @IsString | ๊ฐ์ด ๋ฌธ์์ด์ธ์ง ํ์ธ |
| @IsNumber(options: IsNumberOptions) | ๊ฐ์ด ์ซ์์ธ์ง ํ์ธ |
| @isInt | ๊ฐ์ด ์ ์์ธ์ง ํ์ธ |
| @IsArray | ๊ฐ์ด ๋ฐฐ์ด์ธ์ง ํ์ธ |
| @IsEnum(entity: object) | ๊ฐ์ด ์ ํจํ ์ด๊ฑฐํ์ธ์ง ํ์ธ |
| ์ซ์ Validator | |
| @IsDivisibleBy | ๊ฐ์ด ๋ค๋ฅธ ๊ฐ์ผ๋ก ๋๋ ์ ์๋ ์ซ์์ธ์ง ํ์ธ |
| @isPositive | ๊ฐ์ด 0๋ณด๋ค ํฐ ์์์ธ์ง ํ์ธ |
| @IsNegative | ๊ฐ์ด 0๋ณด๋ค ์์ ์์์ธ์ง ํ์ธ |
| @Min | ์ฃผ์ด์ง ์ซ์๊ฐ ์ฃผ์ด์ง ์ซ์๋ณด๋ค ํฌ๊ฑฐ๋ ๊ฐ์์ง ํ์ธ |
| @Max | ์ฃผ์ด์ง ์ซ์๊ฐ ์ฃผ์ด์ง ์ซ์๋ณด๋ค ์๊ฑฐ๋ ๊ฐ์์ง ํ์ธ |
| ๋ฌธ์ Validator | |
| @Contains(seed: string) | ๋ฌธ์์ด์ seed๊ฐ ํฌํจ๋์ด ์๋์ง ํ์ธ |
| @NotContains(seed: string) | ๋ฌธ์์ด์ seed๊ฐ ํฌํจ๋์ด ์์ง ์์์ง ํ์ธ |
| @IsAlphanumeric | ๋ฌธ์์ด์ ๋ฌธ์์ ์ซ์๋ง ํฌํจ๋์ด ์๋์ง ํ์ธ |
| @IsCreditCard | ๋ฌธ์์ด์ด ์ ์ฉ ์นด๋์ธ์ง ํ์ธ |
| @IsHexColor | ๋ฌธ์์ด์ด 16์ง์ ์์์ธ์ง ํ์ธ |
| @MaxLength | ๋ฌธ์์ด์ ๊ธธ์ด๊ฐ ์ฃผ์ด์ง ์ซ์๋ณด๋ค ๊ธธ์ง ์์์ง ํ์ธ |
| @MinLength | ๋ฌธ์์ด์ ๊ธธ์ด๊ฐ ์ฃผ์ด์ง ์ซ์๋ณด๋ค ์์ง ์์์ง ํ์ธ |
| @IsUUID | ๋ฌธ์์ด์ด UUID(๋ฒ์ 3, 4, 5 ๋๋ ๋ชจ๋ )์ธ์ง ํ์ธ |
| @IsLatLong | ๊ฐ์ ํ์์ด ์๋, ๊ฒฝ๋ ์ธ์ง ํ์ธ |
3. ๋ฐํ ์๋ฌ ๊ตฌ์กฐ
1{2target: Object;3property: string;4value: any;5constraints?: {6[type: string]: string;7};8children?: ValidationError[];9}
target: ๊ฒ์ฆํ ๊ฐ์ฒดproperty: ๊ฒ์ฆ ์คํจํ ํ๋กํผํฐvalue: ๊ฒ์ฆ ์คํจํ ๊ฐconstraints: ๊ฒ์ฆ ์คํจํ ์ ์ฝ์กฐ๊ฑดchildren: ํ๋กํผํฐ์ ๋ชจ๋ ๊ฒ์ฆ ์คํจ ์ ์ฝ ์กฐ๊ฑด
3.1 Class Validator ์๋ฌ๋ฉ์์ง ๋ณ๊ฒฝ
1import { IsString } from 'class-validator'23export class CreatePostDto {4@IsString({5message: 'title์ string ํ์ ์ ์ ๋ ฅํด์ค์ผ ํฉ๋๋ค.',6})7title: string89@IsString({10message: 'content์ string ํ์ ์ ์ ๋ ฅํด์ค์ผ ํฉ๋๋ค.',11})12content: string13}
Decorator์ message ํ๋กํผํฐ์ ๊ฒ์ฆ ์คํจํ์๋์ ์๋ฌ ๋ฉ์ธ์ง๋ฅผ ์
๋ ฅํด์ฃผ๋ฉด ๋๋ค
4. PickType ํ์ฉ
posts ์ํฐํฐ๋ฅผ ํ์ฉํด ์ค๋ณต๋ ์ฝ๋๋ฅผ ๋ ์ค์ผ ์ ์๋ค.
1// ์๋ต23@Entity()4export class PostsModel extends BaseModel {5// ์๋ต6@Column()7@IsString({8message: 'title์ string ํ์ ์ ์ ๋ ฅํด์ค์ผ ํฉ๋๋ค.',9})10title: string1112@Column()13@IsString({14message: 'content์ string ํ์ ์ ์ ๋ ฅํด์ค์ผ ํฉ๋๋ค.',15})16content: string17// ์๋ต18}
create-post DTO๋ Pick ํ์ ์ ํ์ฉํ๋๋ฐ, TS์ ํ์ ์ด ์๋ ๊ฐ์ ๋ฐํํ๋๋ฐ ๋๊ฐ์ ๊ธฐ๋ฅ์ ํ๋ ํด๋์ค๋ฅผ NestJS์์ ์ ๊ณตํด์ค๋ค.
1import { PickType } from '@nestjs/mapped-types'2import { PostsModel } from '../entities/posts.entity'34/***5* Pick, Omit, Partial -> Type์ ๋ฐํ6* PickType, OmitType, PartialType -> ๊ฐ์ ๋ฐํ7*/8export class CreatePostDto extends PickType(PostsModel, ['title', 'content']) {}
5. IsOptional Annotation ์ฌ์ฉ
posts ์ ๋ฐ์ดํธ๋ ์์ ๋ฐ๋ ๊ฐ์ ์ต์ ์ผ๋ก ๋ฐ๊ณ ์๋ค. ์ฌ๊ธฐ์๋ DTO๋ฅผ ์ ์ฉ์ํค์.
1import { IsOptional, IsString } from 'class-validator'2import { CreatePostDto } from './create-post.dto'3import { PartialType } from '@nestjs/mapped-types'45/***6* Pick, Omit, Partial -> Type์ ๋ฐํ7* PickType, OmitType, PartialType -> ๊ฐ์ ๋ฐํ8*/9export class UpdatePostDto extends PartialType(CreatePostDto) {10@IsString()11@IsOptional()12title?: string1314@IsString()15@IsOptional()16content?: string17}
์์ฑํ updateDTO๋ฅผ psots ์ปจํธ๋กค๋ฌ์ ์ ์ฉํ๋ค.
1@Patch(':id')2putPost(3@Param('id', ParseIntPipe) id: number, //4@Body() body: UpdatePostDto,5// @Body('title') title?: string,6// @Body('content') content?: string,7) {8return this.postsService.updatePost(id, body)9}
๋ง์ฐฌ๊ฐ์ง๋ก posts ์๋น์ค์๋ DTO๋ฅผ ์ ์ฉ์ํจ๋ค.
1// posts.service.ts ์๋ต2async updatePost(postId: number, postDto: UpdatePostDto) {3const { title, content } = postDto // ์ถ๊ฐ4const post = await this.postsRepository.findOne({5where: { id: postId },6})78if (!post) throw new NotFoundException()9if (title) post.title = title10if (content) post.content = content1112const newPost = await this.postsRepository.save(post)13return newPost14}
6. Put ์์ฒญ Patch๋ก ๋ณ๊ฒฝ
1// posts.controller.ts2/*** 4) PATCH /posts/:id3* id์ ํด๋นํ๋ post๋ฅผ ๋ถ๋ถ ๋ณ๊ฒฝํ๋ค4*/5@Patch(':id')6patchPost(7@Param('id', ParseIntPipe) id: number, //8@Body() body: UpdatePostDto,9) {10return this.postsService.updatePost(id, body)11}
Put: ์ ๋ฐ์ดํธํ ๊ฐ๋ค์ ์ ๋ถ ์ ๋ ฅํด์ค์ผ ํจ- ๊ทธ ๊ฐ์ฒด๊ฐ ์กด์ฌํ๋ฉด ์ ๋ฐ์ดํธํ๊ณ ,
- ๋ง์ฝ ์กด์ฌํ์ง ์๋ค๋ฉด, ์๋ก ์์ฑํ๋ค.
Patch: ๋ถ๋ถ์ ์ผ๋ก๋ง ์ ๋ ฅ๋ฐ๊ณ ์ ๋ ฅ๋ ๋ถ๋ถ๋ง ์ ๋ฐ์ดํธํ๋ค.- ํฌ์คํธ๋งจ์์๋ put์ patch๋ก ๊ณ ์น๋ค.
7. Length Annotation๊ณผ Email Annotation ์ฌ์ฉ
auth/dto/register-user.dto.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { PickType } from '@nestjs/mapped-types'2import { UsersModel } from 'src/users/entities/users.entity'34export class RegisterUserDto extends PickType(UsersModel, ['nickname', 'email', 'password']) {}
user ์ํฐํฐ๋ฅผ dto๊ฐ ์ฌ์ฉํ ์ ์๊ฒ class-validator, ๊ธฐํ ์ด๋ ธํ ์ด์ ๋ฑ์ ์ถ๊ฐํด ์์ ํ๋ค.
1// ์๋ต23@Entity()4export class UsersModel extends BaseModel {5/*** ๋๋ค์ ํน์ฑ6* 1) ๊ธธ์ด๊ฐ 20์ ๋์ง ์์ ๊ฒ7* 2) ์ ์ผ๋ฌด์ดํ ๊ฐ์ด ๋ ๊ฒ8*/9@Column({10length: 20,11unique: true,12})13@IsString()14@Length(1, 20, {15message: '๋๋ค์์ 1~20์ ์ฌ์ด๋ก ์ ๋ ฅํด์ฃผ์ธ์.',16})17nickname: string1819/*** ์ด๋ฉ์ผ ํน์ฑ20* 1) ์ ์ผ๋ฌด์ดํ ๊ฐ์ด ๋ ๊ฒ21*/22@Column({23unique: true,24})25@IsString()26@IsEmail()27email: string2829@Column()30@IsString()31@Length(3, 8)32password: string3334@Column({35// role ํน์ฑ์ ํ์ ์ RolesEnum์ ๋ชจ๋ ๊ฐ๋ค๋ก ์ง์ 36enum: Object.values(RolesEnum),37default: RolesEnum.USER,38})39role: RolesEnum4041@OneToMany(() => PostsModel, post => post.author)42posts: PostsModel[]43}
๊ทธ๋ฆฌ๊ณ ์์ฑํ DTO๋ฅผ auth ์ปจํธ๋กค๋ฌ์ ์ ์ฉํ๋ค.
1// auth.controller.ts ์๋ต2@Post('register/email')3postRegisterEmail(@Body() body: RegisterUserDto) {4return this.authService.registerWithEmail(body)5}
๋ง์ฐฌ๊ฐ์ง๋ก ์์ฑํ DTO๋ฅผ auth ์๋น์ค์ ์ ์ฉํ๋ค.
1// auth.service.ts ์๋ต2async registerWithEmail(user: RegisterUserDto) {3const hash = await bcrypt.hash(user.password, HASH_ROUNDS)4const newUser = await this.usersService.createUser({5...user, //6password: hash,7})8return this.loginUser(newUser)9}
ํฌ์คํธ๋งจ์์ ํ์ธํ๋ค.
8. Validation Message ์ผ๋ฐํ
๊ณตํต ๋ฉ์์ง๋ฅผ ์์ฑํ common/validation-message/length-validation.message.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { ValidationArguments } from 'class-validator'23/** ValidationArguments์ ํ๋กํผํฐ๋ค4* 1) value -> ๊ฒ์ฆ๋๊ณ ์๋ ๊ฐ(์ ๋ ฅ๋ ๊ฐ)5* 2) constraints -> ํ๋ผ๋ฏธํฐ์ ์ ๋ ฅ๋ ์ ํ์ฌํญ๋ค6* args.constraints[0] -> 17* args.constraints[1] -> 208* 3) targetNmae -> ๊ฒ์ฆํ๊ณ ์๋ ํด๋์ค์ ์ด๋ฆ9* 4) object -> ๊ฒ์ฆํ๊ณ ์๋ ๊ฐ์ฒด10* 5) property -> ๊ฒ์ฆ๋๊ณ ์๋ ๊ฐ์ฒด์ ํ๋กํผํฐ ์ด๋ฆ11* @see https://github.com/typestack/class-validator?tab=readme-ov-file#validation-messages12*/13export const lengthValidationMessage = (args: ValidationArguments) => {14if (args.constraints.length === 2) {15return `${args.property}์ ${args.constraints[0]}~${args.constraints[1]} ๊ธ์๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์!`16} else {17return `${args.property}์ ์ต์ ${args.constraints[0]} ๊ธ์๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์!`18}19}
common/validation-message/string-validation.message.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { ValidationArguments } from 'class-validator'23export const stringValidationMessage = (args: ValidationArguments) => {4return `${args.property}์ string์ ์ ๋ ฅํด์ฃผ์ธ์!`5}
common/validation-message/email-validation.message.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { ValidationArguments } from 'class-validator'23export const emailValidationMessage = (args: ValidationArguments) => {4return `${args.property}์ ์ ํํ ์ด๋ฉ์ผ์ ์ ๋ ฅํด์ฃผ์ธ์!`5}
user ์ํฐํฐ์ ์์ฑํ Validation ๊ณตํต ๋ฉ์์ง๋ฅผ ์ถ๊ฐํ๋ค.
1// ์๋ต23@Entity()4export class UsersModel extends BaseModel {5/*** ๋๋ค์ ํน์ฑ6* 1) ๊ธธ์ด๊ฐ 20์ ๋์ง ์์ ๊ฒ7* 2) ์ ์ผ๋ฌด์ดํ ๊ฐ์ด ๋ ๊ฒ8*/9@Column({10length: 20,11unique: true,12})13@IsString({14message: stringValidationMessage,15})16@Length(1, 20, {17message: lengthValidationMessage,18})19nickname: string2021/*** ์ด๋ฉ์ผ ํน์ฑ22* 1) ์ ์ผ๋ฌด์ดํ ๊ฐ์ด ๋ ๊ฒ23*/24@Column({25unique: true,26})27@IsString({28message: stringValidationMessage,29})30@IsEmail(31{},32{33message: emailValidationMessage,34},35)36email: string3738@Column()39@IsString({40message: stringValidationMessage,41})42@Length(3, 8, {43message: lengthValidationMessage,44})45password: string4647// ์๋ต48}
posts ์ํฐํฐ, update-post.dto์๋ ๊ฐ๊ฐ ์์ฑํ Validation ๊ณตํต ๋ฉ์์ง๋ฅผ ์ถ๊ฐํ๋ค.
1// posts.entity.ts ์๋ต2@Column()3@IsString({4message: stringValidationMessage,5})6title: string78@Column()9@IsString({10message: stringValidationMessage,11})12content: string
1// update-post.dto.ts ์๋ต2export class UpdatePostDto extends PartialType(CreatePostDto) {3@IsString({4message: stringValidationMessage,5})6@IsOptional()7title?: string89@IsString({10message: stringValidationMessage,11})12@IsOptional()13content?: string14}