[미들웨어]DTO - gyunam-bark/nb02-how-do-i-look-team1 GitHub Wiki

설계

DTO_DESIGN

사용법

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);
}