Next.js API 경로 - 전역 오류 처리 및 깨끗한 코드 관행

Next.js는 최고의 React 기반 프레임워크 중 하나입니다. React.js에 대한 학습 곡선이 거의 없고 즉시 사용할 수 있는 뛰어난 SEO를 제공한다는 사실 때문에 많은 개발자들에게 인기 있는 선택이 되었습니다.

파일 시스템 기반 라우팅을 사용하고 API 경로를 생성하는 유연한 방법도 제공합니다. 이는 Next.js에서 간단하게 서버리스 기능을 생성하는 좋은 방법입니다.

그러나 단순성과 유연성으로 인해 다음과 같은 문제가 발생합니다.
  • 요청 방법 확인 및 검증을 위한 중복 코드입니다.
  • 공식 문서에서는 긴 if-else-if 체인을 사용하여 여러 요청 방법을 처리할 것을 제안합니다.
  • API 오류를 처리하기 위한 규칙이 없습니다. 일관된 방식으로 오류를 처리하고(프런트엔드에서 오류를 포착할 수 있도록) 예기치 않은 오류를 기록하는 방법을 제공하기 때문에 이는 문제입니다.

  • 그래서 이러한 문제를 해결하기 위해 모든 중복 코드와 오류 처리를 추상화하여 핵심 비즈니스 논리에 집중할 수 있도록 하는 고차 함수(HOF)를 고안했습니다. 걱정할 필요가 있는 것은 언제 응답을 반환하고 언제 오류를 발생시켜야 하는지입니다. 오류 응답은 전역 오류 처리기(중앙 오류 처리)에서 자동으로 생성됩니다.



    크레딧: 이 방법은 https://jasonwatmore.com/post/2021/08/23/next-js-api-global-error-handler-example-tutorial에서 영감을 받았습니다.

    자, 고민하지 말고 들어가 봅시다.

    챕터


  • Project Setup
  • API Handler Higher Order Function
  • Global Error Handler
  • Creating example API route
  • Wrapping up
  • Source Code

  • 참고: 이것은 DEV에 대한 나의 첫 번째 게시물이므로 제안을 환영하는 것이 아니라 필수입니다! :피


    1. 프로젝트 설정

    Start by creating a Next.js project using the create-next-app npm script. Notice that I'm using the --ts flag to initialize with TypeScript because it's 2022 folks, make the switch!

    npx create-next-app@latest --ts
    # or
    yarn create next-app --ts
    

    We'll need to install two additional packages; one for schema validation (I prefer yup ) and another for declaratively throwing errors, http-errors .

    npm i yup http-errors && npm i --dev @types/http-errors
    # or
    yarn add yup http-errors && yarn add --dev @types/http-errors
    

    2. API 핸들러 고차 함수

    Create a new file ./utils/api.ts . This file exports the apiHandler() function which acts as the entry point for any API route. It accepts a JSON mapping of common HTTP request methods and methodHandler() functions. It returns an async function that wraps all the API logic into a try-catch block which passes all the errors to errorHandler() . More on that later.

    // ./utils/api.ts
    import createHttpError from "http-errors";
    
    import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
    import { Method } from "axios";
    
    // Shape of the response when an error is thrown
    interface ErrorResponse {
      error: {
        message: string;
        err?: any; // Sent for unhandled errors reulting in 500
      };
      status?: number; // Sent for unhandled errors reulting in 500
    }
    
    type ApiMethodHandlers = {
      [key in Uppercase<Method>]?: NextApiHandler;
    };
    
    export function apiHandler(handler: ApiMethodHandlers) {
      return async (req: NextApiRequest, res: NextApiResponse<ErrorResponse>) => {
        try {
          const method = req.method
            ? (req.method.toUpperCase() as keyof ApiMethodHandlers)
            : undefined;
    
          // check if handler supports current HTTP method
          if (!method)
            throw new createHttpError.MethodNotAllowed(
              `No method specified on path ${req.url}!`
            );
    
          const methodHandler = handler[method];
          if (!methodHandler)
            throw new createHttpError.MethodNotAllowed(
              `Method ${req.method} Not Allowed on path ${req.url}!`
            );
    
          // call method handler
          await methodHandler(req, res);
        } catch (err) {
          // global error handler
          errorHandler(err, res);
        }
      };
    }
    
    If a request is received for an unsupported request.method , we throw a 405 (Method Not Allowed) Error . http-errors 를 사용하여 어떻게 오류가 발생하는지 확인하세요. 이것이 비즈니스 로직에서 예상되는 오류를 처리하는 방법입니다. 일관된 오류 응답을 생성하는 프로젝트에서 모든 개발자가 따라야 하는 규칙을 시행합니다.

    참고: 내부 ApiMethodHandlers 에서 Methodaxios 유형을 사용하고 있습니다. axios를 설치하지 않으려면 다음과 같이 정의할 수 있습니다.

    export type Method =
      |'GET'
      |'DELETE'
      |'HEAD'
      |'OPTIONS'
      |'POST'
      |'PUT'
      |'PATCH'
      |'PURGE'
      |'LINK'
      |'UNLINK';
    


    3. 전역 오류 처리기

    When an error is thrown anywhere in our APIs, it'll be caught by the top level try-catch block defined in our apiHandler() (unless ofcourse we define another error boundary somewhere deeper). The errorHandler() checks for the type of error and responds accordingly.

    // ./utils/api.ts
    
    import { ValidationError } from "yup";
    
    
    
    function errorHandler(err: unknown, res: NextApiResponse<ErrorResponse>) {
      // Errors with statusCode >= 500 are should not be exposed
      if (createHttpError.isHttpError(err) && err.expose) {
        // Handle all errors thrown by http-errors module
        return res.status(err.statusCode).json({ error: { message: err.message } });
      } else if (err instanceof ValidationError) {
        // Handle yup validation errors
        return res.status(400).json({ error: { message: err.errors.join(", ") } });
      } else {
        // default to 500 server error
        console.error(err);
        return res.status(500).json({
          error: { message: "Internal Server Error", err: err },
          status: createHttpError.isHttpError(err) ? err.statusCode : 500,
        });
      }
    }
    

    All unforeseen errors are considered unhandled and are presented as 500 Internal Server Error s to the users. I also chose to send the stack trace and log it in this case for debugging.


    4. 예제 API 경로 생성

    With that, our apiHanlder() is complete and can now be used instead of a plain old function export 모든 API 경로 파일 내부.
    실제 작동을 확인하기 위해 데모 경로를 만들어 봅시다.
    가짜 블로그 REST api/api/article?{id: string}를 만들어서 보여드리겠습니다. ./pages/api/ 아래에 파일을 만들고 이름을 article.ts 로 지정합니다.

    // ./pages/api/article.ts
    import createHttpError from "http-errors";
    import * as Yup from "yup";
    
    import { NextApiHandler } from "next";
    
    import { apiHandler } from "utils/api";
    import { validateRequest } from "utils/yup";
    
    // Fake DB to demonstrate the API
    const BLOG_DB = [
      {
        id: 1,
        title: "Top 10 anime betrayals",
        content: "Lorem ipsum dolor sit amet ....",
        publishedTimestamp: 1665821111000,
      },
    ];
    
    type GetResponse = {
      data: typeof BLOG_DB | typeof BLOG_DB[number];
    };
    
    /**
     * returns all articles or the article with the given id if query string is provided
     */
    const getArticle: NextApiHandler<GetResponse> = async (req, res) => {
      const { id } = req.query;
      if (id) {
        // find and return article with given id
        const article = BLOG_DB.find((article) => article.id === Number(id));
    
        if (!article)
          throw new createHttpError.NotFound(`Article with id ${id} not found!`);
        // OR
        // if (!article) throw new createHttpError[404](`Article with id ${id} not found!`)
        res.status(200).json({ data: article });
      } else {
        res.status(200).json({ data: BLOG_DB });
      }
    };
    
    type PostResponse = {
      data: typeof BLOG_DB[number];
      message: string;
    };
    
    const postArticleSchema = Yup.object().shape({
      title: Yup.string().required("Title is required!"),
      content: Yup.string()
        .required("Content is required!")
        .max(
          5000,
          ({ max }) => `Character limit exceeded! Max ${max} characters allowed!`
        ),
      publishedTimestamp: Yup.number()
        .required("Published timestamp is required!")
        .lessThan(Date.now(), "Published timestamp cannot be in the future!"),
    });
    
    const createArticle: NextApiHandler<PostResponse> = async (req, res) => {
      const data = validateRequest(req.body, postArticleSchema);
      const newArticle = { ...data, id: BLOG_DB.length + 1 };
      BLOG_DB.push(newArticle);
    
      res
        .status(201)
        .json({ data: newArticle, message: "Article created successfully!" });
    };
    
    type DeleteResponse = {
      data: typeof BLOG_DB[number];
      message: string;
    };
    
    const deleteArticleById: NextApiHandler<DeleteResponse> = async (req, res) => {
      const { id } = req.query;
    
      if (!id) throw new createHttpError.BadRequest("Article id is required!");
    
      const articleIndex = BLOG_DB.findIndex(
        (article) => article.id === Number(id)
      );
    
      if (articleIndex < 0)
        throw new createHttpError.NotFound(`Article with id ${id} not found!`);
    
      BLOG_DB.splice(articleIndex, 1);
    
      res.status(200).json({
        data: BLOG_DB[articleIndex],
        message: "Article deleted successfully!",
      });
    };
    
    export default apiHandler({
      GET: getArticle,
      POST: createArticle,
      DELETE: deleteArticleById,
    });
    
    validateRequest() 함수는 yup 스키마를 가져와 JSON의 유효성을 검사하는 도우미입니다. 또한 적절한 유형의 검증된 데이터를 반환합니다. 이 검증이 실패하면 errorHandler()에서 처리하는 ValidationError가 발생합니다.

    // ./utils/yup.ts
    
    import { ObjectSchema } from "yup";
    import { ObjectShape } from "yup/lib/object";
    
    export function validateRequest<T extends ObjectShape>(
      data: unknown,
      schema: ObjectSchema<T>
    ) {
      const _data = schema.validateSync(data, {
        abortEarly: false,
        strict: true,
      });
      return _data;
    }
    

    Postman 또는 원하는 다른 도구를 사용하여 방금 생성한 API를 테스트할 수 있습니다.


    5. 마무리

    Notice how clean and descriptive our business logic looks ✨

    res.json() is called only when we need to send a success response.
    ✅ In all other cases we throw the appropriate error contructed by http-errors and leave the rest on the parent function.
    ✅ Controllers are divided into functions and plugged into the API Route by their respective req.method which is kind of reminiscent of how Express.js routes are defined.

    Enforcing clean code practices in our projects increases code readability which matters a lot as the scale of the project starts increasing. 💪🏽

    I don't claim that this is the best way to achieve this goal so if you know of any other better ways, do share! 🙌🏽

    I hope you liked this post and it'll help you in your Next.js projects. If it helped you with a project, I'd love to read about it in the comments. 💜

    Shameless Plug
    Checkout my recently published Modal library for React.js :3


    6. 소스 코드

    은밀한 선생님 / 다음-API-핸들러-예제


    Dev.to의 내 게시물에 대한 동반 예제 프로젝트





    일관성과 깨끗한 코드를 위해 apiHandler 고차 함수를 사용하여 API 경로 생성 🔥

    특징 ⚡


  • res.json()는 성공 응답을 보내야 하는 경우에만 호출됩니다.
  • 오류가 발생하면 apiHandler에서 오류 응답을 처리합니다.
  • 컨트롤러는 함수로 분할되고 각각의 API 경로req.method에 연결됩니다. 이는 Express.js 경로가 정의되는 방식을 연상시킵니다.

  • 기술 🧪


  • Next.js
  • 타입스크립트
  • Yup
  • http-errors

  • 설치 📦


    First, run the development server:
    
    ```bash
    npm run dev
    # or
    yarn dev

    사용해 보세요! 🚀



    Dev.to 📖에서 관련 게시물을 확인하세요.




    View on GitHub

    좋은 웹페이지 즐겨찾기