Express: 오류를 처리하는 확장 가능한 방법

더 좋은 응용 프로그램을 작성하기 위해 오류 처리는 이해가 필요한 중요한 개념이다.그러나, 나는 많은 다른 응용 프로그램에서 오류 처리가 오용되거나 처리되는 것을 자주 본다. 특히, express에서.js 응용 프로그램.
본고에서 우리는 더 좋고 확장 가능한 방식으로 오류를 처리하는 방법을 토론할 것이다.
사용자 입력 검증, 업무 검증 처리, 사용자 등록 저장 등 모든 기능을 갖춘 사용자 등록 API를 구축합니다.

app.post(
  "api/user/registration",
  ...
);

사용자 등록api의 초보자, 고급판, 전문판 등 세 가지 방법을 보여 드리겠습니다.

풋내기 방법


통상적으로, 우리는 어떠한 적당한 설계도 하지 않은 상황에서 다음과 같은 코드 세그먼트를 생성하거나, 이것은 우리가 처음으로 대충 등록 노드를 구축한 것일 수도 있다.

app.post("api/user/registration", postRegistration);

function postRegistration(req, res, next) {
  const {
    first_name,
    last_name,
    email,
    password,
    re_password,
    terms_condition,
  } = req.body;

  const errors = [];

  // Required field validation
  if (!first_name) {
    errors.push("first_name is required");
  }
  if (!last_name) {
    errors.push("last_name is required");
  }
  if (!email) {
    errors.push("email is required");
  }
  if (!email) {
    errors.push("email is required");
  }
  if (!password) {
    errors.push("password is required");
  }
  if (!re_password) {
    errors.push("re_password is required");
  }
  if (!terms_condition) {
    errors.push("terms_condition is required");
  }

  // Length validation
  if (password.length > 8) {
    errors.push("Password has to be longer than 8 characters");
  }

  // cross field match validation
  if (password === re_password) {
    errors.push("Password and re_password has to match");
  }

  // Bad request error
  if (errors.length > 0) {
    return res.status(400).json({ errors: errors });
  }

  // Server business logic validation
  const businessErrors = [];

  if (email.includes("[email protected]")) {
    businessErrors.push("EMAIL_ALREADY_TAKEN");
  }

  if (password.includes("qwerty")) {
    businessErrors.push("AUTH_WEAK_PASSWORD");
  }

  if (businessErrors.length > 0) {
    return res.status(400).json({ businessErrors: businessErrors });
  }

  // await UserRegistrationRepo.register(req.body)
  res.json({ error: false, msg: "Registration is successful" });
}

상술한 방법으로 문제의 소재를 찾아내 봅시다.언뜻 보면 코드 중복, 등록 후 기능에 대한 책임이 너무 많은 문제를 쉽게 발견할 수 있다. 왜냐하면 검증 입력, 처리 업무 검증과 처리 데이터베이스 조작 등 여러 가지 작업을 했기 때문이다.
고급 버전으로 이동합니다.

선진적


이 버전에서 우리는 코드 중복을 없애고 책임을 구분하며 논리적 구분을 없애는 등 우리가 초보적인 방법에서 겪는 문제를 바로잡으려고 시도할 것이다.
app.post(
  "api/user/registration",
  validateRegistrationInput,
  validateBusinessRegistration,
  postRegistration
);
코드의 중복을 피하기 위해, 우리는 리퀴드, minLength 등 서로 다른 규칙을 검증하기 위해 자신의 util 함수를 만들었습니다.
lib/util/validation.회사 명
export function validate(input, validationRule) {
  return Object.keys(validationRule).reduce((errors, key) => {
    const currentRule = validationRule[key];
    if (currentRule.required) {
      if (!input[key]) {
        errors.push(`${key} is required field`);
      }
    }

    if (currentRule.minLength) {
      console.log({ errors, key, currentRule, input });
      if (input[key] && input[key].length < currentRule.minLength) {
        errors.push(
          `${key} has to more than ${currentRule.minLength} characters`
        );
      }
    }
    //TODO:cross field match validation
    return errors;
  }, []);
}

컨트롤러/등록.회사 명
등록된 컨트롤러 코드가 어떤지 봅시다.
import { validate } from './validation'

const validationRule = {
  first_name: {
    required: true,
  },
  last_name: {
    required: true,
  },
  email: {
    required: true,
  },
  password: {
    required: true,
    minLength: 8,
  },
  re_password: {
    required: true,
    ref: "password",
    exactMatch: true,
  },
  terms_condition: {
    required: true,
  },
};

export function validateRegistrationInput(req, res, next) {
  const {
    first_name,
    last_name,
    email,
    password,
    re_password,
    terms_condition,
  } = req.body;

  const errors = validate(req.body, validationRule);

  // Bad request error
  if (errors.length > 0) {
    return res.status(400).json({ errors: errors });
  }
  next();
}

export function validateBusinessRegistration(req, res, next) {
  // Server business logic validation
  const { email, password } = req.body;
  const businessErrors = [];

  if (email.includes("[email protected]")) {
    businessErrors.push("EMAIL_ALREADY_TAKEN");
  }

  if (password.includes("qwerty")) {
    businessErrors.push("AUTH_WEAK_PASSWORD");
  }

  if (businessErrors.length > 0) {
    return res.status(400).json({ errorMessages: businessErrors });
  }

  next();
}

export function postRegistration(req, res, next) {
  // await UserRegistrationRepo.register(req.body)
  res.json({ success: true, data: { message: "Registration is successful" }});
}
api/user/registration 고급 버전의 장단점을 토론해 보겠습니다.
찬성 의견:
  • 코드 중복 감소
  • 청결 분리
  • 단일 책임 고수
  • 사기:
  • 중앙 오류 처리가 사용되지 않음
  • 검증된 자체 실현(모든 용례를 덮어쓰는 구현 및 테스트 용례)
  • 일치하지 않는 오류 모드 구조(클라이언트에게 어떻게 일치하는 오류 모드를 제공합니까?)
  • 자원 상태(400).json({errorMessages:businessErrors});
  • 자원 상태(400).json({errors:errors});
  • 일치하지 않는 응답 모드 구조(클라이언트에게 어떻게 일치하는 응답 모드를 제공합니까?)
  • res.json({success:true, 데이터: {message: "등록 성공"}});
  • res.json({error:false, msg: "등록 성공"});
  • 단지 일치성을 더욱 강조하고 싶을 뿐이다. 왜냐하면 일치성은 더욱 좋고 깨끗하며 이해할 수 있는 코드를 가져오기 때문이다.코드가 혼란스럽지만 코드를 재구성하는 데 도움이 된다.

    Pro: 확장 가능한 오류 처리


    프로 버전에서 우리는 다음과 같은 사항을 주의할 것이다
  • 모드 라이브러리를 사용하여 검증(Yup/Joi)
  • 통합 애플리케이션 오류 인터페이스
  • 사용자 정의 오류 생성
  • 중앙 오류 처리
  • 아키텍처 라이브러리를 사용하여 검증


    예를 들어 Yup/Joi 모델 기반의 검증 라이브러리를 소개하고 싶습니다.등록 노드의 검증 모델을 정의합니다. 아래와 같습니다.
    우리의 userRegistrationSchema 함수를 보십시오.우리가 코드를 많이 작성하지 않은 상황에서javascript 대상에 대한 검증이 얼마나 우아한지 보고 가독성 지식을 생각해 보자. 이것은 이미 많이 개선되었고 패턴을 바탕으로 하는 검증도 우리가 버그를 줄이는 데 도움이 된다!
    검증하다.회사 명
    import * as Yup from "yup";
    
    export function userRegistrationSchema() {
      return Yup.object().shape({
        first_name: Yup.string().required(),
        last_name: Yup.string().required(),
        email: Yup.string().email().required(),
        password: Yup.string()
          .min(8, "Password has to be longer than 8 characters!")
          .required(),
        re_password: Yup.string()
          .oneOf([Yup.ref("password"), null], "Passwords must match")
          .required("Re-enter password is a required field"),
        terms_condition: Yup.boolean().oneOf(
          [true],
          "Please accept terms and conditions"
        ),
      });
    }
    
    

    통합 응용 오류 인터페이스


    응용 프로그램과 클라이언트에게 일치하는 오류 인터페이스, 오류 패턴 구조를 제공하기 위해 전체 응용 프로그램에 자신만의 오류 클래스를 만듭니다.
    다시 말하면, 우리는 사용자 정의 오류 클래스를 만들고, 자바스크립트의 오류 클래스를 확장하는 것을 더 좋아할 수도 있습니다. 아래와 같습니다.
    class ResourceNotFound extend Error { }
    
    이것은 우리의 결정에 달려 있지만, 나는 너무 많은 오류 클래스가 일부 유지보수를 가져왔고, 오류 클래스의 일치성을 강화하는 것은javascript 응용 프로그램에 있어서 필요없을 것 같다고 생각한다.예를 들어, 노드에 있습니다.js 내부 오류는 몇 가지 유형의 오류로 나뉜다.
    ApplicationError 클래스 정의
    lib/api/applicationError.회사 명
    export class ApplicationError extends Error {
      static type = {
        APP_NAME: "APP_NAME",
        INTERNAL: "INTERNAL",
        NETWORK: "NETWORK",
        UNKNOWN: "UNKNOWN",
      };
    
      constructor(options, overrides) {
        super();
        Object.assign(options, overrides);
    
        if (!ApplicationError.type.hasOwnProperty(options.type)) {
          throw new Error(`ApplicationError: ${options.type} is not a valid type.`);
        }
    
        if (!options.message) {
          throw new Error("ApplicationError: error message required.");
        }
    
        if (!options.code) {
          throw new Error("ApplicationError: error code required.");
        }
    
        this.name = "ApplicationError";
        this.type = options.type;
        this.code = options.code;
        this.message = options.message;
        this.errors = options.errors;
        this.meta = options.meta;
        // {
        //   analytics:  {},
        //   context: {}
        // }
        this.statusCode = options.statusCode;
      }
    }
    
    
    좋습니다. 이제 ApplicationError를 정의했습니다. 하지만 Yup의 ValidationError와 ApplicationError는 완전히 다른 인터페이스입니다.
    우리는 어떻게 일치된 오류 인터페이스를 제공합니까?
    Yup 검증이나 Mongo Exception 같은 제3자 이상을 처리하고 있기 때문에 서로 다른 오류 패턴을 가지고 있기 때문에 문제가 발생할 수 있습니다.공장의 기능을 빌려 우리는 우아하게 이 문제를 해결할 수 있다.따라서 기존 코드에 너무 많은 내용을 모르거나 변경하더라도 나중에 Yup을 Joi 또는 다른 내용과 교환할 수 있습니다.
    저희 공장 함수는createError입니다. 제3자 이상이나 오류를 ApplicationError 이상으로 변환하는 것을 책임집니다.여기는 잘못된 공장입니다.js가 밑에 나와요.
    lib/api/errorFactory.회사 명
    import * as Yup from 'yup'
    import { ApplicationError } from './applicationError'
    
    export function createError(error, overrides) {
      const isYupError = error instanceof Yup.ValidationError
      if (isYupError) {
        const yupError = mapYupValidationError(error)
        return new ApplicationError(yupError, overrides)
      }
      return new ApplicationError(error, overrides)
    }
    
    function mapYupValidationError(error) {
    
      return {
        type: ApplicationError.type.APP_NAME,
        code: 'VALIDATION_ERROR',
        message: error.message,
        errors: error.inner,
        statusCode: 400,
        meta: {
          context: error.value
        }
      }
    }
    

    사용자 정의 오류 생성


    등록 API로 돌아가면 등록 노드를 개발할 때 업무 이상이 발생할 수 있습니다.이 몇 가지 예외는
  • 이메일이 수신된 경우(email_already_taken)
  • 사용자가 비암호를 입력한 경우(AUTH\u differ\u password)
  • ...
  • 위에서 말한 바와 같이, 우리는 모든 유형의 오류에 대해 새로운 사용자 정의 오류 클래스를 만들고 싶지 않다.그러면 ApplicationError의 도움으로 사용자 정의 오류를 만드는 방법은 무엇입니까?
    디렉터/등록/오류.회사 명
    
    import { ApplicationError } from '../../lib/api'
    
    export const Errors = {
      EMAIL_ALREADY_TAKEN: {
        type: ApplicationError.type.APP_NAME,
        code: 'EMAIL_ALREADY_TAKEN',
        message: 'The given email address is already taken :(',
        statusCode: 400
      },
      AUTH_WEAK_PASSWORD: {
        type: ApplicationError.type.APP_NAME,
        code: 'AUTH_WEAK_PASSWORD',
        message: 'The given password is easy to guess, provide strong password',
        statusCode: 400
      }
    }
    
    
    앞으로 저희가 다음 방법으로 할 수 있어요.
    new ApplicationError(RegistrationError.EMAIL_ALREADY_TAKEN);
    
    주의해야 할 점은 이러한 업무 검증 오류이다.js가 우리가 등록한 컨트롤러와 함께 사용하는 것은 좋은 일이다.

    보상: 자주 발생하는 오류


    REST API 개발에 도움이 되는 일반적인 오류를 보여 주고 싶습니다.
    lib/api/commonError.회사 명
    import { ApplicationError } from "./applicationError";
    
    const HTTPError = {
      // Predefined 4xx http errors
      BAD_REQUEST: {
        type: ApplicationError.type.NETWORK,
        code: "BAD_REQUEST",
        message: "Bad request",
        statusCode: 400,
      },
      UNAUTHORIZED: {
        type: ApplicationError.type.NETWORK,
        code: "UNAUTHORIZED",
        message: "Unauthorized",
        statusCode: 401,
      },
      FORBIDDEN: {
        type: ApplicationError.type.NETWORK,
        code: "FORBIDDEN",
        message: "Forbidden",
        statusCode: 403,
      },
      RESOURCE_NOT_FOUND: {
        type: ApplicationError.type.NETWORK,
        code: "RESOURCE_NOT_FOUND",
        message: "Resource not found",
        statusCode: 404,
        meta: {
          translationKey: "app.common.error.RESOURCE_NOT_FOUND",
        },
      },
    
      // Predefined 5xx http errors
      INTERNAL_SERVER_ERROR: {
        type: ApplicationError.type.NETWORK,
        code: "INTERNAL_SERVER_ERROR",
        message: "Something went wrong, Please try again later.",
        statusCode: 500,
        meta: {
          shouldRedirect: true,
        },
      },
      BAD_GATEWAY: {
        type: ApplicationError.type.NETWORK,
        code: "BAD_GATEWAY",
        message: "Bad gateway",
        statusCode: 502,
      },
      SERVICE_UNAVAILABLE: {
        type: ApplicationError.type.NETWORK,
        code: "SERVICE_UNAVAILABLE",
        message: "Service unavailable",
        statusCode: 503,
      },
      GATEWAY_TIMEOUT: {
        type: ApplicationError.type.NETWORK,
        code: "GATEWAY_TIMEOUT",
        message: "Gateway timeout",
        statusCode: 504,
      },
    };
    
    export { HTTPError };
    
    

    보상: 응답 모드


    클라이언트에게 일치된 응답 모드를 보내기 위해서는sendResponse라는 함수를 정의하여res.json () 이 아니라sendResponse를 강제로 사용할 수 있도록 해야 할 수도 있습니다
    import { ApplicationError, createError } from '../error'
    
    export function formatError(error, overrides = {}) {
      // `Error.stack`'s `enumerable` property descriptor is `false`
      // Thus, `JSON.stringify(...)` doesn't enumerate over it.
      const stackTrace = JSON.stringify(error, ['stack'], 4) || {}
      const newError = JSON.parse(JSON.stringify(error))
    
      // No need to send to client
      newError.statusCode = undefined
      delete newError.meta
    
      return {
        error: {
          ...newError,
          stack: stackTrace.stack
        },
        success: false,
        ...overrides
      }
    }
    
    export function formatResponse(result, override = {}) {
      return {
        data: result,
        success: true,
        ...override
      }
    }
    
    export function sendResponse(res, payload, statusCode = 200, context = {}) {
      return res.status(statusCode).json(formatResponse(payload))
    }
    
    보시다시피 검증합니다.js와 오류.js가 등록 노드에 협동하여 위치를 정하고 있습니다.

    중앙 오류 처리


    본고의 핵심 기술, 즉 express에서의 집중식 오류 처리를 제시할 때가 되었다.js 응용 프로그램.

    Define error-handling middleware functions in the same way as other middleware functions, except error-handling functions have four arguments instead of three: (err, req, res, next)


    마지막으로, 우리는 다른 응용 프로그램 다음에 오류 처리 중간부품을 정의해야 한다.use () 와routes 호출.
    app.use("/api", userRegistrationRouter);
    
    app.use(errorHandler);
    
    작업 원리

    일반적으로 루트 처리 프로그램과 중간부품 내의 동기화 코드가 잘못되면 추가 작업이 필요하지 않습니다.동기화 코드에서 오류가 발생하면 Express가 캡처하여 처리합니다.
    라우팅 프로세서와 중간부품에서 호출된 비동기 함수로 되돌아오는 오류에 대해 다음 (오류) 함수에 전달해야 합니다. Express는 그 함수를 포착하고 처리합니다.
    아래와 같이 오류를 던지거나 express 중간부품에 전달해야 합니다
    컨트롤러/등록.회사 명
    import { userRegistrationSchema } from "./validation";
    import { createError, sendJson, ApplicationError } from "../../lib/api";
    import { Errors } from "./error";
    
    export async function validateUserRegistration(req, res, next) {
      try {
        await userRegistrationSchema().validate(req.body, { abortEarly: false });
      } catch (e) {
        return next(createError(e));
      }
      next();
    }
    
    export function validationBusinessRule(req, res, next) {
      const { email, password } = req.body;
    
      if (email.includes('[email protected]')) {
        throw new ApplicationError(Errors.EMAIL_ALREADY_TAKEN);
      }
    
      if (password.includes('qwerty')) {
        throw new ApplicationError(Errors.AUTH_WEAK_PASSWORD);
      }
      next()
    }
    export function postRegistration(req, res, next) {
      // await UserRegistrationRepo.register(req.body)
      sendJson(res, { message: "Registration is successful" });
    }
    
    
    validation Business Rule에서 오류를 동기화하기 때문에 like next (오류) 와validate User Registration을 비동기적으로 호출할 필요가 없습니다. 오류를 포획하고 있기 때문에 like next (오류) 를 통해 express 중간부품을 포획합니다.
    다음은 우리의 집중식 오류 중간부품이다
    라이브러리/오류 처리 프로그램.회사 명
    import { sendResponse, formatError, CommonError } from "../lib/api";
    
    export function errorHandler(err, req, res, next) {
        const { analytics = {} } = err.meta || {};
      // logging for analytics
      console.log({ analytics });
    
      if (err instanceof ApplicationError) {
        const code = err.statusCode || 500
        return res.status(code).json(formatError(err))
      }
    
      if (err instanceof Error) {
        const newError = createError(err)
        const code = newError.statusCode || 500
        return res.status(code).json(formatError(newError))
      }
    
      const unknownError = new ApplicationError(CommonError.UNKNOWN_ERROR)
    
      return sendResponse(res, unknownError, statusCode);
    }
    
    
    가장 중요한 것은 우리가 모든 중간부품의 오류를 처리하지 않았다는 것이다. 모든 오류 처리는 집중식 오류 중간부품으로 옮겨졌다. 목적은 우리가 다른 장면을 쉽게 포괄할 수 있는 큰 기회가 있다는 것이다. 예를 들어
  • 오류 세부 정보 기록
  • 상세 정보 전송 분석
  • 포맷 그룹 오류 모드 오류
  • 마지막으로 다음 cURL 명령을 사용하여 등록 노드를 테스트합니다.
    curl --location --request POST 'http://localhost:3000/api/user/registration' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'first_name=raja' \
    --data-urlencode 'last_name=jaganathan' \
    --data-urlencode 'password=qwerty1234' \
    --data-urlencode 're_password=qwerty1234' \
    --data-urlencode '[email protected]' | python -mjson.tool
    
    {
        "error": {
            "name": "ApplicationError",
            "type": "APP_NAME",
            "code": "AUTH_WEAK_PASSWORD",
            "message": "The given password is easy to guess, provide strong password"
        },
        "success": false
    }
    
    그렇습니다.좋아!!!
    너는 이곳에서 환매 협의를 찾을 수 있다💌 https://github.com/RajaJaganathan/express-error-handling
    읽어주셔서 감사합니다!

    좋은 웹페이지 즐겨찾기