[미들웨어]DTO - gyunam-bark/nb02-how-do-i-look-team1 GitHub Wiki
설계
사용법
1. dto-middleware.js 에서 적용하려는 엔드포인트 구조체를 확인합니다.
이 문서 혹은 dto-middleware.js 코드 가장 아래에 모든 엔드포인트 구조체가 정리되어 있습니다.
export default {
validateRequest,
// STYLE
createStyleSchema,
getStyleListSchema,
updateStyleSchema,
getStyleDetailSchema,
deleteStyleSchema,
// RANKING
getRankingListSchema,
// CURATION
createCurationSchema,
getCurationListSchema,
updateCurationSchema,
deleteCurationSchema,
// COMMENT
createCommentSchema,
updateCommentSchema,
deleteCommentSchema,
};
2. *-routers.js 에서 validateRequest 와 위에서 확인한 엔드포인트 구조체를 import 합니다.
아래의 예시는 Ranking 을 기준으로 합니다.
import { validateRequest, getRankingListSchema } from '../middlewares/dto-middleware.js';
router.get('/', validateRequest(getRankingListSchema), rankController.getRankingList.bind(rankController));
Ranking 에서는 엔드포인트가 '/' 로 하나이기 때문에 하나의 엔드포인트 스키마 구조체를 가져옵니다.
만약 여러 개의 엔드포인트가 있을 경우 그 갯수만큼 가져와야 합니다!
3. *-controllers.js 에서 req 대신 req.validate 객체를 사용합니다.
req.body, req.query, req.params 대신 req.validate.body, req.validate.query, req.validate.params 를 사용합니다.
위와 마찬가지로 아래는 Ranking 의 예시입니다.
const { page = 1, pageSize = 10, rankBy = 'total' } = req.validated.query;
스키마 제한사항 상수 목록
숫자 범위 상수
const ID_MIN = 1;
const PAGE_MIN = 1;
const PAGE_SIZE_MIN = 0;
const SORT_BY_ENUMS = {
LATEST: 'latest',
MOST_VIEWED: 'mostViewed',
MOST_CURATED: 'mostCurated',
};
const SEARCH_BY_STYLE_ENUMS = {
NICKNAME: 'nickname',
TITLE: 'title',
CONTENT: 'content',
TAG: 'tag',
};
const SEARCH_BY_CURATION_ENUMS = {
NICKNAME: 'nickname',
CONTENT: 'content',
};
const RANK_BY_ENUMS = {
TOTAL: 'total',
TRENDY: 'trendy',
PERSONALITY: 'personality',
PRACTICALITY: 'practicality',
COST_EFFECTIVENESS: 'costEffectiveness',
};
const KEYWORD_MIN = 1;
const KEYWORD_MAX = 32;
const NICKNAME_MIN = 1;
const NICKNAME_MAX = 32;
const TAG_MIN = 1;
const TAG_MAX = 16;
const TITLE_MIN = 1;
const TITLE_MAX = 64;
const CONTENT_MIN = 1;
const CONTENT_MAX = 256;
const PASSWORD_MIN = 4;
const PASSWORD_MAX = 16;
const NAME_MIN = 1;
const NAME_MAX = 64;
const BRAND_MIN = 1;
const BRAND_MAX = 64;
const PRICE_MIN = 0;
const IMAGE_URL_MIN = 1;
const IMAGE_URL_MAX = 2048;
const TRENDY_MIN = 0;
const PERSONALITY_MIN = 0;
const PRACTICALITY_MIN = 0;
const COST_EFFECTIVENESS_MIN = 0;
COERCE 정의
const stringToInteger = (value) => {
const integer = Number(value);
if (!Number.isInteger(integer)) {
throw new Error(`${value} is not an integer`);
}
return integer;
};
const integer = coerce(number(), string(), stringToInteger);
파라미터 정의
const id = min(integer, ID_MIN);
const page = min(integer, PAGE_MIN);
const pageSize = min(integer, PAGE_SIZE_MIN);
const sortBy = enums(Object.values(SORT_BY_ENUMS));
const searchByStyle = enums(Object.values(SEARCH_BY_STYLE_ENUMS));
const searchByCuration = enums(Object.values(SEARCH_BY_CURATION_ENUMS));
const rankBy = enums(Object.values(RANK_BY_ENUMS));
const keyword = size(string(), KEYWORD_MIN, KEYWORD_MAX);
const tag = size(string(), TAG_MIN, TAG_MAX);
const tags = array(tag);
const nickname = size(string(), NICKNAME_MIN, NICKNAME_MAX);
const title = size(string(), TITLE_MIN, TITLE_MAX);
const content = size(string(), CONTENT_MIN, CONTENT_MAX);
const password = size(string(), PASSWORD_MIN, PASSWORD_MAX);
const name = size(string(), NAME_MIN, NAME_MAX);
const brand = size(string(), BRAND_MIN, BRAND_MAX);
const price = min(integer, PRICE_MIN);
const imageUrl = size(string(), IMAGE_URL_MIN, IMAGE_URL_MAX);
const imageUrls = array(imageUrl);
const trendy = min(integer, TRENDY_MIN);
const personality = min(integer, PERSONALITY_MIN);
const practicality = min(integer, PRACTICALITY_MIN);
const costEffectiveness = min(integer, COST_EFFECTIVENESS_MIN);
// NESTED PARAMETER
const category = object({
name: name,
brand: brand,
price: price,
});
const categories = object({
top: optional(category),
bottom: optional(category),
outer: optional(category),
dress: optional(category),
shoes: optional(category),
bag: optional(category),
accessory: optional(category),
});
스키마 구조체
Style
// POST:STYLE
export const createStyleSchema = {
body: object({
nickname: nickname,
title: title,
content: content,
password: password,
categories: categories,
tags: optional(tags),
imageUrls: optional(imageUrls),
}),
query: object({}),
params: object({}),
};
// GET:STYLE_LIST
export const getStyleListSchema = {
body: object({}),
query: object({
page: optional(page),
pageSize: optional(pageSize),
sortBy: optional(sortBy),
searchBy: optional(searchByStyle),
keyword: optional(keyword),
tag: optional(tag),
}),
params: object({}),
};
// PUT:STYLE
export const updateStyleSchema = {
body: object({}),
query: object({}),
params: object({
styleId: id,
}),
};
// DELETE:STYLE
export const deleteStyleSchema = {
body: object({}),
query: object({}),
params: object({
styleId: id,
}),
};
// GET:STYLE
export const getStyleDetailSchema = {
body: object({}),
query: object({}),
params: object({
styleId: id,
}),
};
Ranking
// GET:RANKING
export const getRankingListSchema = {
body: object({}),
query: object({
page: optional(page),
pageSize: optional(pageSize),
rankBy: optional(rankBy),
}),
params: object({}),
};
Curation
// POST:CURATION
export const createCurationSchema = {
body: object({
nickname: nickname,
content: content,
password: password,
trendy: trendy,
personality: personality,
practicality: practicality,
costEffectiveness: costEffectiveness,
}),
query: object({}),
params: object({
styleId: id,
}),
};
// GET:CURATION_LIST
export const getCurationListSchema = {
body: object({}),
query: object({
page: optional(page),
pageSize: optional(pageSize),
searchBy: optional(searchByCuration),
keyword: optional(keyword),
}),
params: object({
styleId: id,
}),
};
// PUT:CURATION
export const updateCurationSchema = {
body: object({
nickname: nickname,
content: content,
password: password,
trendy: trendy,
personality: personality,
practicality: practicality,
costEffectiveness: costEffectiveness,
}),
query: object({}),
params: object({
curationId: id,
}),
};
// DELETE:CURATION
export const deleteCurationSchema = {
body: object({}),
query: object({}),
params: object({
curationId: id,
}),
};
Comment
// POST:COMMENT
export const createCommentSchema = {
body: object({
content: content,
password: password,
}),
query: object({}),
params: object({
curationId: id,
}),
};
// PUT:COMMENT
export const updateCommentSchema = {
body: object({
content: content,
password: password,
}),
query: object({}),
params: object({
commentId: id,
}),
};
// DELETE:COMMENT
export const deleteCommentSchema = {
body: object({
password: password,
}),
query: object({}),
params: object({
commentId: id,
}),
};
미들웨어
export const validateRequest = (schema = {}) => {
return async (req = {}, _res = {}, next) => {
try {
req.validated = {
body: schema.body ? create(req.body ?? {}, schema.body) : undefined,
query: schema.query ? create(req.query ?? {}, schema.query) : undefined,
params: schema.params ? create(req.params ?? {}, schema.params) : undefined,
};
// NEXT TO CONTROLLER
next();
} catch (error) {
if (error instanceof StructError) {
error.statusCode = 400;
error.message = undefined;
}
// NEXT TO ERROR
next(error);
}
};
};
시행착오
1. 기본적으로 제공받는 req 객체는 getter 로만 접근할 수 있다.
req.body, req.query. req.params 와 같이 express 에서 기본적으로 제공하는 내부 객체는 임의로 수정할 수 없다.
즉, 아래와 같은 코드는 에러를 발생한다.
req.query = schema.query ? create(req.query ?? {}, schema.query) : undefined
그래서 req 내부에 validated={} 를 새로 만들어서 그 안에 넣어서 넘겨주는 방식을 통해서 Controller 에 전달한다.
req.validated = {
body: schema.body ? create(req.body ?? {}, schema.body) : undefined,
query: schema.query ? create(req.query ?? {}, schema.query) : undefined,
params: schema.params ? create(req.params ?? {}, schema.params) : undefined,
};
2. coerce 를 사용할 때엔 create 를 사용할 것.
superstruct 의 coerce 는 들어온 값을 지정한 형식에 따라 변경해서 검증하는 기능이다.
그래서 문자열 '1' 로 들어온 요청 파라미터를 숫자 1 로 변형해서 검증한 값을 전달한다.
다만 validate 와 달리 error 를 내부에서 자체적으로 반환하기 때문에 try{}catch(error){} 에서 별도로 체크 후 에러를 전달한다.
catch (error) {
if (error instanceof StructError) {
error.statusCode = 400;
}
// NEXT TO ERROR
next(error);
}