nestjs response dto, nestjs request dto μμ±λ²
Nestjs μμ Rest API server λ§λ€ λ μ¬μ©νλ©΄ μ μ©ν class validator request DTO, response DTO μ¬μ©λ²μ μμλ³΄κ² μ΅λλ€.
NestJS λ‘ RestAPI server λ₯Ό λ§λ€λ€ 보면 DTO μ μ μ©ν¨μ μκ²λλ€.
DTO λ (Data Transfer Object) κ³μΈ΅κ° λ°μ΄ν° κ΅νμ μν΄ Data λ₯Ό λ³ννμ¬ μ¬μ©νλ κ°μ²΄λ₯Ό μλ―Ένλ€. μ¬κΈ°μ κ³μΈ΅κ°μ΄λ client μ server κ°μΌμλ μκ³ , server logic κ³Ό data repository κ°μΌμλ μλ€.
Nestjs μ ν¨κ» μ°κΈ° μν DTO λΌμ΄λΈλ¬λ¦¬λ class validator κ° μλ€. 곡μλ¬Έμμμλ class-validator λ₯Ό μ°κ³ μλ€.
μ΄ κΈμμλ ν΄λΌμ΄μΈνΈ-μλ² κ³μΈ΅ κ°μμ μ¬μ©λλ DTO λ₯Ό λ€λ€λ³Ό κ²μ΄λ€.
μ§κΈ νλ‘μ νΈμμλ ν΄λΌμ΄μΈνΈκ° μμ²μ λ³΄λΌ λμ μλ΅μ λ°μ λ νμν DTO λ₯Ό request.dto μ response.dto λ‘ κ΅¬λΆν΄μ μ°κ³ μλ€.
1. nestjs request DTO
μν OTT μλΉμ€λΌκ³ κ°μ ν΄λ³΄μ. μνλ₯Ό μλ‘ μΆκ°νλ api μλ μΈμκ°μΌλ‘ μνμ λν μ 보λ₯Ό λ£μ΄ μλ²μ 보λ΄μΌ ν κ²μ΄λ€. ν΄λΌμ΄μΈνΈμμ μν μ λͺ©, μ¬λ€μΌ μ΄λ―Έμ§, κ°λ΅ μκ°λ₯Ό μ λ ₯ν΄ μλ²μ λ³΄λΌ λ POST /movie/create λΌλ api λ₯Ό νΈμΆνλ©΄μ body λ‘ μν μ λͺ©, μ¬λ€μΌ μ΄λ―Έμ§, κ°λ΅ μκ°λ₯Ό λ΄μ 보λΈλ€.
ν΄λΌμ΄μΈνΈμμ Request λ₯Ό λ³΄λΌ λλ ν΄λΌμ΄μΈνΈμμ λ³΄λΈ body κ° μ ν¨ν μ 보λ€μ λ΄κ³ μλ μ§ μλ²μμ λ°λμ νμΈν΄μΌ νλ€. μλ₯Ό λ€λ©΄ κ°λ΅ μκ°κΈμμλ μ΅λ 100μλ₯Ό λμ΄μλ μ λλλ° ν΄λΌμ΄μΈνΈμμ 100μ λκ² μ λ ₯ν΄ λ³΄λμ λ μλ²μμ μ΄λ₯Ό κ±Έλ¬μ£Όμ΄μΌ νλ€. λ¬Όλ‘ , ν΄λΌμ΄μΈνΈμμ μ λ ₯ν λ 100μκ° λμΌλ©΄ submit μ λͺ»νκ² λ§μ μλ μμ§λ§, curl λ± api λ₯Ό λ°λ‘ νΈμΆνλ κ²½μ°λ₯Ό λλΉν΄ μ΄μ€μΌλ‘ μλ²μμλ λ§μμ£Όλ κ²μ΄ μ’λ€.
λν νλμ μλ²μ λ κ° μ΄μμ ν΄λΌμ΄μΈνΈκ° μλ κ²½μ°, POST /movie/create λΌλ νλμ api λ₯Ό νΈμΆν λμλ ν΄λΌμ΄μΈνΈλ€μ΄ Request body νμμ λ§μΆ°μ£Όμ΄μΌ νλλ°, DTO κ° μμ κ²½μ° api μλΉμ€ λ‘μ§ νΈμΆ μ μ body λ₯Ό validate ν΄μ€μΌλ‘μ¨ ν΄λΌμ΄μΈνΈλ€μ΄ μ λλ‘ νμμ λ§μΆμλ μ§ validate ν ν error λ₯Ό throw ν΄μ€λ€.
nestjs request DTO μμ±λ² μμ
src/domains/movie/dto/create-movie.request.dto import { ApiProperty } from '@nestjs/swagger'; import { IsBoolean, IsEnum, IsString, Length, Validate } from 'class-validator'; import { MovieType } from '../enum/movie-type.enum'; export class CreateMovieRequestDto { @ApiProperty() @IsString() @Length(1, 30, { message: 'movie μ λͺ©μ 1μ μ΄μ 30μ μ΄λ΄μ¬μΌ ν©λλ€.' }) movieTitle: string; @ApiProperty() @IsString() thumbnailImage: string; @ApiProperty() @IsEnum(MovieType) movieType: MovieType; @ApiProperty() @IsString() @MaxLength(100) description: string; }
src/main.ts app.useGlobalPipes( new ValidationPipe({ // decorator κ° λ¬λ¦° field λ§ μ ν¨μ± κ²μ¬λ₯Ό νκ² λ€λ μ΅μ whitelist: true, // string μΌλ‘ λ€μ΄μ€λ param μ entity type μ λ§μΆ° λ³νμμΌμ£Όλ μ΅μ . // μ) id: number κ° param μ string μΌλ‘ λ€μ΄μλ number λ‘ λ³νμμΌ controller μ λκ²¨μ€ transform: true, exceptionFactory(errors) { const errorProperties = errors.map((error) => error.property); return new BadRequestException( // ex) "message": "Validation failed: ~", `Validation failed: ${errorProperties.join(',')}`, ); }, }), );
exceptionFactory λ₯Ό μ¬μ©νλ©΄ validator λ₯Ό ν΅κ³Όνμ§ λͺ»ν΄ λ΄λ±λ μλ¬ λ©μΈμ§λ₯Ό custom ν μ μλ€.
2. nestjs response DTO
Response DTO λ μ μ©νλ€. μ§κΈ νλ‘μ νΈμμ NestJS + TypeORM μ μ°κ³ μλλ°,
MovieModule (MovieService, MovieRepository) μμ movie κ΄λ ¨ data λ₯Ό κ°μ Έμ¬ λ this.MovieRepository.find κ°μ ν¨μλ€μ νΈμΆνλ€. μ΄λ κ°μ Έμμ§λ data λ movie.entity.ts μ μ μΈν λ°μ΄ν°λ€μ΄λ€.
μν OTT μλΉμ€λ₯Ό λ§λ€ λ μν λͺ©λ‘ λ§ κ°μ Έμ€λ κ²½μ°κ° μκ³ , μν μμΈ μ 보λ₯Ό κ°μ Έμ€λ κ²½μ°κ° μλ€.
GET /movie/list λ₯Ό λ§λ€ λλ this.MovieRepository.findAll μ ν κ²μ΄κ³ ,
GET /movie/:movieId λ₯Ό λ§λ€ λλ this.MovieRepostory.find λ₯Ό ν κ²μ΄λ€.
λ λ€ movie entity μμ κ°μ Έμ€λ κ²μ΄κΈ° λλ¬Έμ λ°λ‘ selet μ μ λ£μ§ μλ μ΄μμ entity ν΅μ±λ‘ κ°μ Έμ¨λ€.
κ·Έλ°λ° ν΄λΌμ΄μΈνΈμμ μν λͺ©λ‘μ 보μ¬μ€ λλ movieTitle, thumbnailImage λ§ νμνλ° movieType, description κΉμ§ κ°μ Έμ¬ νμκ° μλ€. μ΄λ΄ λ response.dto κ° μ μ©νλ€.
ν¬μΈνΈλ, κ°μ movie entity λ₯Ό μ¬μ©νμ§λ§ μν λͺ©λ‘μ 보μ¬μ€ λλ MovieListInfoResponseDto λ₯Ό λ§λ€μ΄ μ¬μ©νκ³ , μν detail λ°μ΄ν°κ° νμν λλ MovieDetailResponseDto λ₯Ό ꡬλΆμ§μ΄ λ§λλ κ²μ΄λ€.
src/domains/movie/dto/movie-list-info.response.dto import { IsString } from 'class-validator'; export class MovieListInfoResponseDto { @IsString() readonly movieTitle: string; @IsString() readonly thumbnailImage: string; @Exclude() @IsString() readonly description: string; @Exclude() @IsEnum() readonly movieType: MovieType }
src/domains/movie/dto/movie-detail.response.dto import { IsEnum, IsString, Exclude } from 'class-validator'; import { MovieType } from '../enum/movie-type.enum'; export class MovieDetailResponseDto { @IsString() readonly movieTitle: string; @IsString() readonly thumbnailImage: string; @IsString() readonly description: string; @Exclude() @IsEnum() readonly movieType: MovieType }
@Exclude() λ°μ½λ μ΄ν°λ₯Ό λΉΌκ³ μΆμ νλμ λΆμ¬μ£Όλ©΄ λλ€.
μμ²λΌ dto λ₯Ό λ§λ€κ³ , μλΉμ€ λ‘μ§μμ μλμ²λΌ ν΄μ£Όλ©΄ λλ€.
/src/domain/movie/service.ts import { plainToInstance } from 'class-transformer'; import { MovieListInfoResponseDto } from './dto/movie-list-info.response.dto async getMovieList = () => { ... const movies = await this.movieRepository.findAll({ where: { ... } }) return plainToInstance(MovieListInfoResponseDto, movies); }
κ²°κ³Ό
{ movieTitle: βAvatar2β, thumbnailImage: β~β}
π‘π‘π‘π©π»βπ»π©π»βπ»
nestjs response DTO, nestjs request DTO λ₯Ό μμ°κ³ λ this.movieRepositor.find() μμ selet λ₯Ό μΈ μλ μμ§λ§, κ°μΈμ μΌλ‘λ DTO λ₯Ό μ¬μ©ν΄ κ²°κ³Όκ°μ κ°κ³΅νλ κ²μ΄ μ’ λ μ μ§λ³΄μμ μ’μ λ°©λ²μ΄λΌκ³ μκ°μ΄ λ€μλ€. ν΄λΌμ΄μΈνΈμκ² λλ €μ£Όλ κ²°κ³Όκ°μ κ°κ³΅νλ κ³³μ λ°μ΄ν°λ² μ΄μ€ μΏΌλ¦¬κ° μλλΌ ν΄λΌμ΄μΈνΈμ μλ²κ° λ§λλ μ μ μμ μ΄λ£¨μ΄μ§λ κ²μ΄ μν μ± μ μμ¬λ₯Ό λͺ νν ν μ μκΈ° λλ¬Έμ΄λ€.
Reference
https://docs.nestjs.com/techniques/validation