【Next.js13】Next.js APIの60秒のタイムアウトを解消する

こんにちは。プログラミングスクール[Happiness Chain](https://happiness-chain.com/)でメンターをしているryoです。

今回はVercel利用でのNext.jsのAPIを使用する際に、タイムアウト上限60秒を超えてエラーとなる問題を解消する方法を紹介します。こちらの記事はNext.js13対応の記事になります。
参考記事は以下になります。
https://www.defer.run/blog/nextjs-timeout-error

## 実装イメージ
実装イメージは以下になります。

まず、1つ目のエンドポイントで重い処理を実行開始します。
重い処理は非同期で処理され、実行IDが返却されます。
![スクリーンショット 2023-10-02 17.27.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634526/943f7765-ced8-0d40-a95c-68f54041f018.png)

その後、返却されたIDを含めて2つ目のエンドポイントを叩き、
その処理の処理状況と、もし処理が完了していれば、処理結果が返却されます。
![スクリーンショット 2023-10-02 17.27.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634526/a3ebfe0b-13b6-1dff-7c73-61d8a9e53336.png)

## 実装環境
node 19.8.1
next 13.5.3


## deferのsetup
deferというパッケージを使います。
```
npm i @defer/client
```
プロジェクト直下にdeferというディレクトを作成し、
longProcessという重い処理の関数を作成します。
今回は6秒後に文字列を返すようにしています。
```defer/longProcess.ts
define a background function
import { defer } from "@defer/client";

async const longProcess = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      const now = new Date();
      resolve(
        `Hello World ${now.getHours()}:${now.getMinutes()}:${now.getMinutes()}`
      );
    }, 6000);
  });
}

// the function must be wrapped with `defer()` and exported as default
export default defer(longProcess);

```

また、next.config.jsを編集する必要があります。
```next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverMinification: false,
  },
};

module.exports = nextConfig;
```

## Next.js APIを作成
next.js13のappディレクトリ構成にて作成します。
エンドポイントとしては、
`/api/hello`
`/api/hello/[executionId]`
とします。

そのため、
`/app/api/hello/route.ts`
`/app/api/hello/[executionId]/route.ts`
を作成します。

`/api/hello`を叩くと重い処理が実行され、レスポンスとして実行ID（executionId）が返却されます。
そして、`/app/api/hello/[executionId]/route.ts`を叩くと、処理が実行中の場合は、`state`に`start`が入ったobjectが返却され、処理が完了している場合は、`state`に`succeed`が入り、resultに処理結果が格納されます。

```/app/api/hello/route.ts
import type { NextApiRequest, NextApiResponse } from "next";

import longProcess from "../../../defer/longProcess";

export const GET = async (
  _req: NextApiRequest,
  _res: NextApiResponse
) => {
  const data = await longProcess();

  return new Response(JSON.stringify(data));
};

```

```/app/api/hello/[executionId]/route.ts
import { getExecution } from "@defer/client";
import type { NextApiRequest } from "next";

type ParamsType = {
  params: { executionId: string };
};

export const GET = async (_req: NextApiRequest, { params }: ParamsType) => {
  const { executionId } = params
  const ret = await getExecution(executionId as string);
  return new Response(JSON.stringify(ret));
};
```

# 呼び出し部分を作成

```src/app/page.tsx
"use client";

import { useState } from "react";

export default function Home() {
  const [executionId, setExecutionId] = useState("");
  const [status, setStatus] = useState("");
  const [result, setResult] = useState("");

  const handleSubmitStart = async () => {
    setResult("");
    setStatus("");
    const res = await fetch("/api/hello", { method: "GET" });
    const data = await res.json();
    setExecutionId(data.id);
  };

  const handleSubmitInquiry = async () => {
    const res = await fetch(`/api/hello/${executionId}`, {
      method: "GET",
    });
    const data = await res.json();
    if (data == null) return;
    setStatus(data.state);
    if (data.state === "succeed") {
      setResult(data.result);
    }
  };

  return (
    <div>
      <h1>ExecutionId: {executionId}</h1>
      <h1>Status: {status}</h1>
      <h1>Result: {result}</h1>
      <button type="button" onClick={handleSubmitStart}>
        Start
      </button>
      <button type="button" onClick={handleSubmitInquiry}>
        Inquiry
      </button>
    </div>
  );
}

```
上記実装にて、Startボタンを押した際に、処理が開始され、
![スクリーンショット 2023-10-02 17.14.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634526/93c772aa-c482-f79a-0400-bd27a7f2a2da.png)
Inquiryボタンを押すと、対象の処理の状態を取得でき、
![スクリーンショット 2023-10-02 17.14.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634526/92d3ffb8-9848-d640-ad88-b203459bd88e.png)
処理が終わっている場合は、結果を取得することができます。
![スクリーンショット 2023-10-02 17.15.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634526/f2b7f3b0-89d0-d071-b0b8-51e7346027c0.png)

今回は処理結果の取得をボタンを押して取得していますが、
useEffect、setIntervalを使用して、定期的に取得するようにするのが良いかと思います。