【予期せぬ大量のAPIリクエストに注意】Next.js SSGページのビルド時に429エラー(アクセス過多)が生じた

# 初めに

フロント：Next.js(TypeScript)
API：Laravel(PHP)

こちらの環境でSSGのブログページを作った所、ビルド時に429エラー（アクセス過多）が生じました。
当ページではエラーが起こった原因と、その対処法についてまとめております。

意図せず、大量のAPIリクエストを行ってしまいアカウントBAN等にならないよう注意喚起の意味も含め投稿致しました。

# 429エラーが生じる可能性のあるやり方

JSONPlaceholderのAPIを用いまして解説致します。
ソースコードはこちら。

```ruby:[id].tsx
import { GetStaticPaths, GetStaticProps } from 'next';

interface BlogData {
  userId: number;
  id: number;
  title: string;
  body: string;
}

interface BlogDataProps {
  getData: BlogData;
}

const BlogPage = ({ getData }: BlogDataProps) => {
  return (
    <div>
      <p>タイトル</p>
      <h1>{getData?.title}</h1>
      <p>内容</p>
      <p>{getData?.body}</p>
    </div>
  );
};

export default BlogPage;

export const getStaticPaths: GetStaticPaths = async () => {
  // JSONPlaceholderの投稿データを取得
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  const datas: BlogData[] = await response.json();

  // 各投稿のIDをパスとして使用
  const paths = datas.map((data) => ({
    params: { id: data.id.toString() },
  }));

  return { paths, fallback: false };
};

export const getStaticProps: GetStaticProps<BlogDataProps> = async ({ params }) => {
  const id = params?.id; // 'id'はnullかもしれないので'?'を使用して安全に取得する

  if (!id) {
    return { notFound: true }; // 'id'がundefinedの場合、404エラーを返す
  }

  // JSONPlaceholderから特定の投稿を取得
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const getData: BlogData = await response.json();

  return { props: { getData } };
};
```


こちらのコードでもローカル環境であれば普通に表示されます。
例えば http://localhost:3000/blog/1 にアクセスした場合です。
![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2713225/048d296a-a2d2-f899-d73d-8a4adbead0e3.jpeg)

静的なhtmlページを生成するために`npm run build` を実行した所、以下のエラーが生じました。

```ruby:BASH
Error occurred prerendering page "/blog/8482782". Read more: https://nextjs.org/docs/messages/prerender-error
AxiosError: Request failed with status code 429
    at settle (file:///プロジェクト名/frontend/nextjs/node_modules/axios/lib/core/settle.js:19:12)
    at IncomingMessage.handleStreamEnd (file:///プロジェクト名/frontend/nextjs/node_modules/axios/lib/adapters/http.js:556:11)
    at IncomingMessage.emit (node:events:525:35)
    at endReadableNT (node:internal/streams/readable:1359:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
info  - Generating static pages (316/316)

```
API側で429エラーが発生してページ情報の取得に失敗しています。


# エラーの原因

SSGは、ビルド時に１個１個のhtml形式のページを数十ミリ秒単位で繰り返し作成します。
先程のやり方では1ページ分の情報を、その都度APIリクエストしているようです。
該当の箇所は`getStaticProps`の部分です。

```ruby:[id.tsx]
export const getStaticProps: GetStaticProps<BlogDataProps> = async ({ params }) => {
  const id = params?.id; // 'id'はnullかもしれないので'?'を使用して安全に取得する

  if (!id) {
    return { notFound: true }; // 'id'がundefinedの場合、404エラーを返す
  }

  // JSONPlaceholderから特定の投稿を取得
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const getData: BlogData = await response.json();

  return { props: { getData } };
};
```

そのため、ページ数が多くなってくるとAPIがlaravelの場合は「短期間に沢山アクセスしないで」という意味の429エラーが返って来ます。
また、1ページをビルドするたびにブログのSQLが発行されるためサーバーに負荷がかかってしまいます。
いわゆる`n+1問題`ですね。

# 429エラーの対処法

ざっくりと処理のやり方をまとめますと、一度のAPIリクエストでブログの全情報を取得して、その情報を用いて１個１個のページをビルドを行います。

以下が、コードになります。
```ruby:[id].tsx
import { GetStaticPaths, GetStaticProps } from 'next';

interface BlogData {
  userId: number;
  id: number;
  title: string;
  body: string;
}

interface BlogDataProps {
  getData: BlogData | undefined;
}

const BlogPage: React.FC<{ getData: BlogData }> = ({ getData }) => {
  return (
    <div>
      <p>タイトル</p>
      <h1>{getData?.title}</h1>
      <p>内容</p>
      <p>{getData?.body}</p>
    </div>
  );
};

export default BlogPage;

// データを一度だけ取得
let cachedData: BlogData[] | null = null;

const fetchData = async () => {
  if (cachedData) {
    return cachedData;
  }

  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  const datas: BlogData[] = await response.json();

  // データをキャッシュ
  cachedData = datas;

  return cachedData;
};

export const getStaticPaths: GetStaticPaths = async () => {
  const datas = await fetchData();

  if (!datas) {
    // データが取得できない場合のエラー処理を行うか、適切な処理を実行してください
  }

  // 各投稿のIDをパスとして使用
  const paths = datas.map((data) => ({
    params: { id: data.id.toString() },
  }));

  return { paths, fallback: 'blocking' };
};

export const getStaticProps: GetStaticProps<BlogDataProps> = async ({ params }) => {
  const dataId = params?.id;

  // すでにキャッシュされたデータを使用
  const datas = await fetchData();

  //配列からページidの情報だけを使う
  const getData = datas.find((data) => {
    return data.id == Number(dataId);
  });

  return {
    props: {
      getData: getData,
    },
  };
};

```
こちらのやり方の重要な部分は以下になります。
```ruby:[id].tsx
// データを一度だけ取得
let cachedData: BlogData[] | null = null;

const fetchData = async () => {
  if (cachedData) {
    return cachedData;
  }

  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  const datas: BlogData[] = await response.json();

  // データをキャッシュ
  cachedData = datas;

  return cachedData;
};
```
日本語化すると
キャッシュデータがない場合はAPIリクエストを行い全ブログデータをキャッシュする。
もし、キャッシュデータがすでに存在する場合は、そのままキャッシュデータを使う。

コチラの`fetchData`関数を`GetStaticPaths`と`getStaticProps`の両方で使います。

# キャッシュデータを使う部分の処理

ページをビルドする際に先程の`fetchData`を用いてキャッシュデータをフェッチします。

キャッシュデータは単ページが複数、配列で格納されています。
`props`には、そのページだけの情報を渡す必要があるため`find`メソッドを使いページidで単ページのデータに絞り込みます。

```ruby:[id].tsx
export const getStaticProps: GetStaticProps<BlogDataProps> = async ({ params }) => {
  const dataId = params?.id;

  // すでにキャッシュされたデータを使用
  const datas = await fetchData();

  //配列からページidの情報だけを使う
  const getData = datas.find((data) => {
    return data.id == Number(dataId);
  });

  return {
    props: {
      getData: getData,
    },
  };
```

# 再ビルドを行ってみる

ビルドできました。※パスは気にしないで下さい。
![aa.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2713225/bf9ead30-ff60-c3b5-a45c-4ef310872f9e.jpeg)

# ブログデータが大きすぎるとメモリーサイズのエラーが発生します

こちらのやり方は全ブログデータを取得しているため大規模なブログサイトではAPI側でメモリーサイズオーバーのエラーが発生する場合がございます。
その場合はブログデータを分割して取得し、Nextでもそれに対応させる処理を考える必要がござます。

laravelであれば、eloquantの`paginate`メソッドを使えば簡単に分割取得できて便利です。

```ruby:sample.php
use App\Models\Blog;
//ブログの情報を100件ずつ取得
$blogs = Blog::paginate(100);
```

# まとめ

SSGのページは爆速ですが、ビルド時のデメリットが多々ございます。
その一例として、今回紹介したように`getStaticProps`の部分でAPIを叩きますとビルド時に大量のAPIリクエストを行う可能性があることを頭の片隅に置いておくといいかもしれません。