이번 글에서는 타입스크립트 백엔드 프레임워크인 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 작업을 하는 방법을 알아보았다.