Абстракция для валидации параметров запроса

~/utils/express-validation-middleware.ts

import {
  Request as IRequest,
  Response as IResponse,
  NextFunction as INextFunction,
} from 'express'
import { THelp } from './interfaces'

// NOTE: Соглашение facebook like
// Бэк присылает ответ в виде { ok: boolean; message?: string; <Возможно, чо-то еще> }

type TProps = {
  rules: THelp
}

export const withReqParamsValidationMW = ({ rules }: TProps) =>
  (req: IRequest, res: IResponse, next: INextFunction) => {
  // -- NOTE: Errs handler
  const errs: { msg: string, _reponseDetails?: any }[] = []

  for (const reqProp in rules.params) {
    switch (reqProp) {
      case 'body':
      case 'query':
        for (const key in rules.params[reqProp]) {
          if (rules.params[reqProp][key]?.required && !req[reqProp][key]) {
            const validationResult = rules.params[reqProp][key]?.validate(req[reqProp][key])
            const errOpts: any = {
              msg: `Missing required param: \`req.${reqProp}.${key}\` (${rules.params[reqProp][key].type}: ${rules.params[reqProp][key].descr})${!!validationResult.reason ? ` | ⚠️ By developer: ${validationResult.reason}` : ''}`
            }
            
            if (!!validationResult._reponseDetails)
              errOpts._reponseDetails = validationResult._reponseDetails

            errs.push(errOpts)
          } else {
            // -- NOTE: Если имеется необязательный параметр, проверим его
            if (!!req[reqProp][key]) {
              try {
                const validationResult = rules.params[reqProp][key]?.validate(req[reqProp][key])

                if (!validationResult.ok) {
                  const errOpts: {
                    msg: string;
                    _reponseDetails?: {
                      status: number;
                      [key: string]: any;
                    }
                  } = {
                    msg: `Incorrect request param format: \`req.${reqProp}.${key}\` (${rules.params[reqProp][key].descr}) expected: ${rules.params[reqProp][key].type}. Received: ${typeof req[reqProp][key]}${!!validationResult.reason ? ` | ⚠️ By developer: ${validationResult.reason}` : ''}`,
                  }
                  if (!!validationResult._reponseDetails)
                    errOpts._reponseDetails = validationResult._reponseDetails

                  errs.push(errOpts)
                }
              } catch (err) {
                errs.push({
                  msg: `Не удалось проверить поле: \`req.${reqProp}.${key}\` (${rules.params[reqProp][key].descr}); ${typeof err === 'string' ? err : (err.message || 'No err.message')}`
                })
              }
            }
            // --
          }
        }
        break
      default:
        break
    }
  }

  if (errs.length > 0) {
    let status = 400 // NOTE: Or anything by default?
    const result: any = {
      ok: false,
      message: `⛔ ERR! ${errs.map(({ msg }) => msg).join('; ')}`,
      _service: {
        originalBody: req.body,
        originalQuery: req.query,
        rules,
      }
    }

    // -- NOTE: Пробуем добавить детали первой ошибки результатов обработки в ответ (если они есть)
    try {
      // NOTE: v1 Добавить, если они имеются только у превой ошибки
      // if (!!errs[0]._reponseDetails) {
      //   const { status: newStatus, _addProps } = errs[0]._reponseDetails
      //   status = newStatus
      //   if (!!_addProps && Object.keys(_addProps).length > 0) {
      //     for (const key in _addProps) result[key] = _addProps[key]
      //   }
      // }

      // NOTE: v2 А если для этой нет? Нужно ли добавить детали хотя бы для одной ошибки из имеющихся?
      let _reponseDetails: any = null
      for (const err of errs) {
        if (!!err._reponseDetails) {
          _reponseDetails = err._reponseDetails
          break
        }
      }
      if (!!_reponseDetails) {
        const { status: newStatus, _addProps } = _reponseDetails
        status = newStatus
        if (!!_addProps && Object.keys(_addProps).length > 0) {
          for (const key in _addProps) result[key] = _addProps[key]
        }
      }
    } catch (err) {
      result._service.message = typeof err === 'string' ? err : (err.message || 'No err.message')
    }
    // --
    
    return res.status(status).send(result)
  }
  // --

  return next()
}

~/utils/types.ts

export type TValidationResult = {
  ok: boolean;
  reason?: string;
  _reponseDetails?: {
    status: number;
    _addProps?: {
      [key: string]: any;
    }
  }
}

export type THelp = {
  params: {
    body?: {
      [key: string]: {
        type: string; // NOTE: Это просто для информации (не строка с типом js)
        descr: string;
        required: boolean;
        validate: (val: any) => TValidationResult;
      }
    }
    query?: {
      [key: string]: {
        type: string;
        descr: string;
        required: boolean;
        validate: (val: any) => TValidationResult;
      }
    }
  }
  res?: {
    [key: string]: any;
  }
}

route-example.ts

import { TValidationResult } from './types'

export const rules = {
  params: {
    body: {
      // NOTE: Перечисляем поля в теле ответа в качестве ключей...
      text: {
        type: 'string',
        descr: 'Target input text',
        required: true,
        validate: (val: any) => {
          const result: TValidationResult = {
            ok: true,
          }
          
          switch (true) {
            case !val || typeof val !== 'string':
              result.ok = false
              result.reason = 'Ожидается непустая строка'
              break
            default:
              break
          }
          result._reponseDetails = {
            status: 200,
          }
          return result
        }
      }
    }
  }
}

export const getSlugify = (req: IRequest, res: IResponse) => {
  const { text } = req.body
  const _result: any = { ok: true }

  try {
    // @ts-ignore
    _result.result = slugify(text)
    return res.status(200).json({ ok: true, ..._result })
  } catch (err) {
    return res.status(200).json({ ok: false, message: err?.message || 'No err.message' })
  }
}

Usage example

import express, { Express as IExpress } from 'express'
import { getSlugify, rules as getSlugifyRules } from './route-example'
import { withReqParamsValidationMW } from '~/utils/express-validation-middleware'

const careServiceApi: IExpress = express()

careServiceApi.post(
  '/get-slugify',
  withReqParamsValidationMW({ rules: getSlugifyRules }),
  getSlugify,
)

export { careServiceApi }

// NOTE: POST /get-slugify { text: 'Example' } -> { ok: true, <Что-то еще> }
// NOTE: POST /get-slugify { text: '' } -> { ok: false, message: '⛔ ERR! Incorrect request param format: `req.body.text` (Target input text) expected: string. Received: string | ⚠️ By developer: Ожидается непустая строка' }