Next.js + Redoc で1秒以内でレスポンスする API 仕様書をつくった

こんにちは zane です。

[Collections](https://collections.dev/ja) というオープンソースのヘッドレス CMS をつくっています。今回パブリックな API 仕様書が必要になり Next.js(Nextra) + Redoc という組み合わせを試したら、なかなか良かったので手順としてまとめてみました。

OpenAPI(Swagger)などを使った API 仕様書はだいたいどのプロジェクトでも作成しますが、それを公開するとなると、機会はあまり多くはないかもしれません。が、大きく技術トレンドが変わる分野でもないので、覚えておくといずれ役に立つかもしれません！

## 完成品
![api.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421112/3bdb566c-83f6-dc99-7c9e-65afcc6933f5.png)

公開されているページは[こちら](https://collections.dev/reference/api)

コードはすべて GitHub で公開していますので、参考にしてみてください。
https://github.com/collectionscms/collections/blob/main/docs/pages/reference/api.ja.mdx

## Redoc とは？
OpenAPI 仕様で記述された yaml ファイルから HTML ファイルを生成し、静的なドキュメントにしてくれるツールで、オープンソースとしても公開されています。

https://github.com/Redocly/redoc

Collections は、ドキュメント全体を Nextra （Next.jsベースの静的サイトジェネレーター） で作っていますので、これに統合してレンダリングさせています。

Nextra については、以前記事を書いていますので、参考にしてみてください。
https://zenn.dev/zane/articles/0cf5b21d49125b

## React のコンポーネントとして Redoc をインストールする
それでは早速、[公式のステップ](https://redocly.com/docs/redoc/deployment/react/) に沿って進めていきましょう。まずは `@redocly/cli` と関連する依存関係をインストールします。

```
npm i react react-dom mobx styled-components core-js
```

## yaml ファイルを準備
次に、OpenAPI 仕様をもとに yaml ファイルを作成します。
Collections の API 仕様書を貼っておきますので、参考してみてください。

https://github.com/collectionscms/collections/blob/main/docs/data/api.yaml

VSCode を使っている方は `OpenAPI (Swagger) Editor` を入れておくと、文法エラーなどワーニングを出してくれるのでサクサク作業が進められます。

https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi

## コンポーネントに追加する
ファイルの準備ができたらページに Redoc のコンポーネントを埋めると、自動で HTML を生成して出力してくれます！

```
import { RedocStandalone } from 'redoc';

<RedocStandalone specUrl="url/to/your/spec"/>
```

ただし `specUrl` で**ローカルファイルを参照させることはできません**。これは GitHub のイシューでもやりとりされていますが、同一オリジンポリシーとセキュリティ上の都合によるものです。
https://github.com/Redocly/redoc/issues/149#issuecomment-359285569

この問題の対応策として S3 などに yaml を退避させる選択肢もありますが、手間は増えるしファイルをアップロードするために CI のジョブを追加したりと、あまりやりたくはありません。。。

## Next.js の API で配信し SSG 化する
そのため Next.js のAPIを使って yaml をレスポンスさせ、それを渡すことでこの問題を解決しました。

~~~ts:openapi.ts
export default function handler(_req: NextApiRequest, res: NextApiResponse<ResponseData>) {
  const fullPath = path.join(dataDirectory, 'api.yaml');
  const fileContents = fs.readFileSync(fullPath, 'utf8');
  res.status(200).json(YAML.parse(fileContents));
}
~~~

まぁ、これだけでも良かったのですが、都度リクエストが走ることでレンダリングが結構遅い。。。そのため、もう一工夫が必要です。Redoc には`spec`という URL の代わりにオブジェクトを渡すプロパティもあるので

```
export const getStaticProps = ({ params }) => {
  return fetch(`https://collections.dev/api/openapi`)
    .then((res) => res.json())
    .then((repo) => ({
      props: {
        ssg: repo,
      },
    }));
};

export const Redoc = () => {
  const data = useData();
  return <RedocStandalone spec={data} />;
};
```
このように SSG で事前レンダリングしてオブジェクトを渡すことで、見事レスポンス面もクリアできました！

## パフォーマンス

```
$ curl 'https://collections.dev/ja/reference/api/' -H 'Accept-Encoding: gzip, deflate, sdch' -H 'Accept-Language: en-US,en;q=0.8,ja;q=0.6' -H 'Upgrade-Insecure-Requests: 1' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.86 Safari/537.36' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' -H 'Connection: keep-alive' --compressed -o /dev/null -w  "%{time_starttransfer}\n" -s

0.239587
0.249859
0.255718
0.249946
0.253883
```
と、だいたい 300ms 以内でレスポンスされます。

![speed](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421112/1941ddb5-dc49-3e48-e37a-52eb150728a0.png)

`First Contentful Paint` も1秒くらい。これ以上改善しても SEO が求められるページでもないので、合格点を出してもいいのではないでしょうか！

## 最後に
ドラッグ＆ドロップで WordPress を API 化するオープンソースのヘッドレス CMS 【 [Collections](https://collections.dev/ja) 】をつくっています！

Markdown(with GFM) やダークモードなど、書き心地の良さも持ち味です。 [Live Demo](https://demo.collections.dev) も公開していますので、ぜひ気軽に触ってみてください 🙌🙌

それでは