Node에서는 유저가 보내주는 Requst 데이터를 검증하기 위해 class-validator를 사용한다.
class-validator는 Dto에 정의한 타입 정보를 기반으로 유저가 보내준 데이터를 class로 감싼 다음 타입에 맞는지 검증한다.
공식 홈페이지 예제에서는 특정 Dto class를 생성한다음 Dto 객체에 각각 req.body를 넣어주는 방식으로 되어있는데 각 컨트롤러마다 그렇게 넣어주고 싶지는 않았다..
class-transformer로 특정 Dto Class 객체를 바로 생성 할 수 있었는데, 이 경우 발생하는 문제를 공유하고 안전하게 받는 방법에 대해 정리했다.
export class Post {
@Length(10, 20)
title: string;
@Contains('hello')
text: string;
@IsInt()
@Min(0)
@Max(10)
rating: number;
@IsEmail()
email: string;
@IsFQDN()
site: string;
@IsDate()
createDate: Date;
}
let post = new Post();
post.title = 'Hello'; // should not pass
post.text = 'this is a great post about hell world'; // should not pass
post.rating = 11; // should not pass
post.email = 'google.com'; // should not pass
post.site = 'googlecom'; // should not pass
validate(post).then(errors => {
// errors is an array of validation errors
if (errors.length > 0) {
console.log('validation failed. errors: ', errors);
} else {
console.log('validation succeed');
}
});
validateOrReject(post).catch(errors => {
console.log('Promise rejected (validation failed). Errors: ', errors);
});
// or
async function validateOrRejectExample(input) {
try {
await validateOrReject(input);
} catch (errors) {
console.log('Caught promise rejection (validation failed). Errors: ', errors);
}
}
나는 각각 api 컨트롤러에서 검증을 하기보다는 미들웨어로 만들고 검증을 하고 싶었기 때문에 이렇게 하고 싶지 않았다.
Dto
import { IsNumber, IsString, Min } from 'class-validator';
import { Expose } from 'class-transformer';
export class TestDto {
@Expose()
@IsNumber()
public userIdx: number;
@Expose()
@IsString()
public userName: string;
}
검증 미들웨어
import { plainToClass } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';
export default function validationMiddleware(type: any, skipMissingProperties = false): RequestHandler {
return (req, res, next) => {
validate(plainToClass(type, req.body), { skipMissingProperties }).then((errors: ValidationError[]) => {
req.body = plainToClass(type, req.body, { excludeExtraneousValues: true });
if (errors.length > 0) {
const message = errors.map((error: ValidationError) => Object.values(error.constraints || error.children[0].children)).join(', ');
next(new HttpException(400, message));
} else {
next();
}
});
};
}
이렇게 유저가 API에 데이터를 보내면 먼저 검증을 할 수 있는 미들웨어를 구성했는데,
여기서 plainToclass는 class-transformer 로 유저에게 날라오는 request 데이터를 Class로 객체로 전체를 바꿔준다.
다만, Dto에 선언하지 않은 데이터도 바꿔주는데 내 경우 이게 큰 문제로 다가왔다. Class 선언해서 한땀한땀 넣어줬으면 이런 문제는 없었겠지만..
문제가 되는 부분
{
"userIdx": 1,
"userName": "hello"
}
이렇게 날라올땐 문제가 없다
{
"userIdx": 1,
"userName": "hello",
"test": "asdqwe123" // 이건 dto에 넣어두지 않았는데 객체속에 같이 넣어진다는 것
}
“test” 는 내가 dto에 선언하지 않았지만 class-transofrmer 에서 자동으로 추가시켜서 넘겨준다.
모든 DTO에 데코레이터로 Expose를 추가해주고 미들웨어 검증에서는 plainToClass에서 환경변수로 excludeExtraneousValues 를 넣어준다
DTO
import { IsNumber, IsString, Min } from 'class-validator';
import { Expose } from 'class-transformer';
export class TestDto {
@Expose()
@IsNumber()
public userIdx: number;
@Expose()
@IsString()
public userName: string;
}
import { plainToClass } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';
export default function validationMiddleware(type: any, skipMissingProperties = false): RequestHandler {
return (req, res, next) => {
validate(plainToClass(type, req.body, {excludeExtraneousValues: true }), { skipMissingProperties }).then((errors: ValidationError[]) => { // 이 부분
req.body = plainToClass(type, req.body, { excludeExtraneousValues: true }); // 이 부분
if (errors.length > 0) {
const message = errors.map((error: ValidationError) => Object.values(error.constraints || error.children[0].children)).join(', ');
next(new HttpException(400, message));
} else {
next();
}
});
};
}
아직 아쉬운 부분이 있지만 이렇게 하면 해결.
그런데 버그라서 사용 할 수 없음;