이번 글에서는 타입스크립트 백엔드 프레임워크인 Nest.js에서 POST나 PUT과 데이터 요청이 들어왔을때 그 데이터가 적절한 데이터인지 확인하는 validation과
GET을 했을때 서버 데이터에서 가지고 있는 raw데이터 형식이 아니라 중간에서 Interceptor하여 다른 형태의 데이터로 변환 시켜 줄 수 있는 transform을 한번 해보려고 한다.
먼저 테스트 하기 위한 데이터를 ts파일에서 넣는다.
Test Data
export type ReportType = 'income' | 'expense';
export interface Reports {
  reports: {
    id: string;
    source: string;
    amount: number;
    created_at: Date;
    updated_at: Date;
    type: ReportType;
  }[];
}
export const data: Reports = {
  reports: [
    {
      id: 'uuid',
      source: 'Salary',
      amount: 7500,
      created_at: new Date(),
      updated_at: new Date(),
      type: 'income',
    },
    {
      id: 'uuid232',
      source: 'Salary',
      amount: 7500,
      created_at: new Date(),
      updated_at: new Date(),
      type: 'income',
    },
    {
      id: 'uuid2',
      source: 'Youtube',
      amount: 300,
      created_at: new Date(),
      updated_at: new Date(),
      type: 'expense',
    },
    {
      id: 'uuid3',
      source: 'Facebook',
      amount: 7500,
      created_at: new Date(),
      updated_at: new Date(),
      type: 'expense',
    },
  ],
};데이터 구조는 아래와 같이 이루어져있고, POST를 통해 데이터를 삽입하는 것으로 예시를 들어보겠다.
Controller
@Post()
  createReport(@Param('type') pType: ReportType, @Body() body: Report) {
    return this.appService.createReport(pType, body);
  }Service
createReport(pType: ReportType, body: Report) {
    if (!isDataType(pType)) {
      return { message: '잘못된 타입' };
    }
    const newReport: Reports['reports'][number] = {
      id: uuid(),
      created_at: new Date(),
      updated_at: new Date(),
      type: pType,
      ...body,
    };
    data.reports.push(newReport);
    return data;
  }문제점
위와 같은 형식으로 컨트롤러를 처리하면 아직 validation이 안되는 상태이기때문에 잘못된 데이터가 들어와도 에러가 발생하지 않고 컨트롤러 진입하게된다.
위 요청에 따른 응답값 (맨 아래 보면 잘못된 데이터가 들어가 리턴된 모습을 볼 수 있다 )
{
    "reports": [
        {
            "id": "uuid",
            "source": "Salary",
            "amount": 7500,
            "created_at": "2022-05-22T02:07:35.647Z",
            "updated_at": "2022-05-22T02:07:35.647Z",
            "type": "income"
        },
        {
            "id": "uuid232",
            "source": "Salary",
            "amount": 7500,
            "created_at": "2022-05-22T02:07:35.647Z",
            "updated_at": "2022-05-22T02:07:35.647Z",
            "type": "income"
        },
        {
            "id": "uuid2",
            "source": "Youtube",
            "amount": 300,
            "created_at": "2022-05-22T02:07:35.647Z",
            "updated_at": "2022-05-22T02:07:35.647Z",
            "type": "expense"
        },
        {
            "id": "uuid3",
            "source": "Facebook",
            "amount": 7500,
            "created_at": "2022-05-22T02:07:35.647Z",
            "updated_at": "2022-05-22T02:07:35.647Z",
            "type": "expense"
        },
        {
            "id": "d283f562-7598-41ab-8321-4a3ce28c0b7b",
            "created_at": "2022-05-22T02:07:40.157Z",
            "updated_at": "2022-05-22T02:07:40.157Z",
            "type": "income",
            "amount": -1,
            "source": ""
        }
    ]
}Dto를 사용하여 Validation 하기
만약 오로지 양수인 amount와 반드시 입력된 source가 있는 데이터를 받고 그 외 데이터가 들어오면 오류를 발생시키려면 어떻게 해야할까?
src → dto → report.dto 생성 후 아래와 같이 입력
import {
  IsNotEmpty,
  IsNumber,
  IsPositive,
  IsString,
} from 'class-validator';
export class CreateReportDto {
  @IsNumber()
  @IsPositive()
  amount: number;
  @IsString()
  @IsNotEmpty()
  source: string;
}IsNumber → 오직 숫자만
IsPositive → 오직 양수만
IsString → 오직 문자만
IsNotEmpty → 무조건 값이 필요
해당 Dto를 컨트롤러에 적용
body타입에 해당 Dto 인스턴스 타입을 넣어준다
@Post()
  createReport(
    @Param('type') pType: ReportType,
    @Body() body: CreateReportDto,
  ) {
    return this.appService.createReport(pType, body);
  }요청이 들어오고 응답을 Interceptor하여 변형된 응답값을 리턴해주고 싶을땐 어떻게 해야할까?
아이디 값을 하나 받아 하나의 데이터를 리턴해주는 GET
컨트롤러
@Get(':id')
  getReportById(@Param('type') pType: ReportType, @Param('id') pId: string) {
    return this.appService.getReportById(pType, pId);
  }서비스
getReportById(pType: ReportType, pId: string): ReportResponseDto {
    const result = data.reports
      .filter(({ type }) => type === pType)
      .find(({ id }) => id === pId);
    return result;
  }응답값
만약 위 처럼 서버에 저장되어있는 데이터 형태 그대로 말고 좀 다르게 변형된 형태로 return 해주고 싶으면 어떻게 해야할까?
예를들어 updated_at이라는 데이터는 응답이 안내려가고 created_at이라는 키는 camel case로 createdAt으로 내려주고 싶다면 말이다.
이것또한 Dto를 사용하여 만들어 줄 수 있다.
Dto Class 만들기
export class ReportResponseDto {
  id: string;
  source: string;
  amount: number;
  @Expose({ name: 'createdAt' })
  created_at: Date;
  @Exclude()
  updated_at: Date;
  type: ReportType;
  constructor(partial: Partial<ReportResponseDto>) {
    Object.assign(this, partial);
  }
}서비스 로직
getReportById(pType: ReportType, pId: string): ReportResponseDto {
    const result = data.reports
      .filter(({ type }) => type === pType)
      .find(({ id }) => id === pId);
    return new ReportResponseDto(result);
  }Trasnform과 Intercept를 위한 환경 설정
main.ts 에 옵션 추가
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      transformOptions: {
        enableImplicitConversion: true,
      },
    }),
  );
  await app.listen(3000);
}
bootstrap();app.module.ts
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: ClassSerializerInterceptor,
    },
  ],
})
export class AppModule {}이상 Dto의 개념과 dto를 사용하여 validation과 transform 작업을 하는 방법을 알아보았다.