【Next.js】ページごとに異なるAPIを呼び出して見せ方を変える実装メモ

自分のところの`/api/`配下のAPIを実行してデータのやり取りをするアプリケーションにおいて、いろんなAPIを汎用的に呼び出せるようにしたうえで、フロント側の見せ方はページごとに変えたいと思っていた。
調べた感じズバッとこれだ！っていうのがなかったのだが、色々やった感じで以下の方法でうまく動作したので、メモとして残す。

# 単一APIの利用

## テスト用のAPIを用意

```javascript:src/pages/api/hello.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  res.status(200).json({ name: 'John Doe' })
}

```

- `npx create-next-app --typescript`で作成したときにできるやつをそのまま使用。
- 中身は実際なんでもいい。ここでは動作確認用のためこれを使用する。

## APIを実行して結果を加工して返却するためのcomponent

```javascript:src/components/api-component.tsx
import useSWR from 'swr';

const fetcher = async (uri: string) => {
    const response = await fetch(uri);
    return response.json();
};

export function ApiComponent(uri: string, RF: React.FC<any>) {
    const { data, error } = useSWR(uri, fetcher);
    if (error) return <div>oops... {error.message}</div>;
    if (data === undefined) return <div>Loading... with React.FC</div>;
    return <RF data = { data } />;
}
```

- swrに関しては[こちら](https://swr.vercel.app/ja)の実装をほぼそのまま流用。全てのAPIの実行をこれに寄せることで、エラー発生時やロード中の表示を統一できる。
- APIの実行結果を加工と書いているが、加工処理の実態は引数で渡された`RF: React.FC<any>`に委ねられている。そしてこれはこの`ApiComponent`を呼び出す側から渡される形。
- `React.FC`の型を`<any>`にしているのは、いろんなAPIの結果（いろんなオブジェクトの形）を受け取ることを意識して。統一できそうにないなと思ったので。統一の型があるならその型を指定する。


## APIの実行指示と実行結果の画面編集を行うpage

```javascript:src/pages/test.tsx
import type { NextPage } from 'next'
import { ApiComponent } from '../components/api-component';

const Test: NextPage = () => {
    return ApiComponent('/api/hello' , TestContent);
}

type Props = {
    data: {
        name: string
    }
};

const TestContent: React.FC<Props> = (props) => {
    const { data } = props;
    return (
        <>
            <p>name: {data.name}</p>
        </>
    );
}

export default Test;
```

- `ApiComponent`には、引数としてAPIの実行パス（`'/api/hello'`）と画面表示内容のReactComponent定義（`TestContent`）を渡す。
- APIの実行パスは一番上で書いたやつを呼びだす。実際のプロジェクトの実態に応じて変更要
- `TestContent`は実際に画面にどう表示するかの編集ロジック。`ApiComponent`の引数に渡して、編集してもらって結果を返してもらう。処理の内容自体はここで定義。つまり、ページごとに定義する。この例では`<p>{JSON.stringify(data)}</p>`と、非常に簡単な処理しか実装してないが、各ページの仕様に従って色々加工する。
- `Props`のtype定義は呼び出すAPIの返却オブジェクトの仕様をあらわしている。ここはAPIごとに変わるので適宜変更。ここに書くよりはAPI側のほうにtype定義してそれをExportさせて使う方がいいかもしれない。
- これだけだと単に`name: John Doe`としか表示しないので、実際には`layout` componentとかと組み合わせて画面内の適切な箇所に適宜表示するように`TestContent`内の定義を変更する必要がある

# 追記：複数APIの利用

上のは特定のページで単一のAPIのみ利用するパターンだが、単一のページで複数のAPIを使いたい場合どうするか？というのをメモする。基本的には変わらない。

## テスト用のAPIを用意

ちょっと違うAPIを用意するがリクエストパラメータを貰う以外はほぼ↑と同じ。

```typescript:src/pages/api/test.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export type TestAPIType = {
  text: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<TestAPIType>
) {
  const query = req.query;
  const text = String(query.text||'(no-data)');

  res.status(200).json({ text: text });
}
```

- リクエストパラメータとして `text` を受け取ってそれをそのままレスポンスするだけのAPI。リクエストパラメータを使ったのは、これから「複数のAPI」を利用する想定にあたり、それぞれのAPI呼び出しがちゃんと動作しているかを確認するためのもので、機能上の意味は全くない。なんならその数の分だけAPI用意しても良い。（本来の開発においてはそっちのほうが用途が多そうではある）
- ジェネリクスで使用している `TestAPIType` が `text: string` で定義しているため、 `const text = String(query.text||'(no-data)');` とわざわざ `String` で囲っている（ `NextApiRequest#query` が `string | string[]` なので、そのまま使うとType定義に反してしまう）と思ったけどだったら `TestAPIType` を `text: string|string[]` にすりゃいいだけか。。まあいいや。

## APIを実行して結果を加工して返却するためのcomponent

```typescript:src/pages/components/test.tsx
import { ApiComponent } from './api-component';
import { TestAPIType } from '../pages/api/test';

export const Test1 = () => {
    return ApiComponent('/api/test?text=test1' , TestContent1);
};

export const Test2 = () => {
    return ApiComponent('/api/test?text=test2' , TestContent2);
};

export const Test3 = () => {
    return ApiComponent('/api/test?text=test3' , TestContent3);
};

type Props = {
    data: TestAPIType
}

const TestContent1: React.FC<Props> = (props) => {
    const { data } = props;
    return (
        <>
            <div>
                <p>test1</p>
                <p>text: {data.text}</p>
            </div>
        </>
    );
}

const TestContent2: React.FC<Props> = (props) => {
    const { data } = props;
    return (
        <>
            <div>
                <p>test2</p>
                <p>text: {data.text}</p>
            </div>
        </>
    );
}

const TestContent3: React.FC<Props> = (props) => {
    const { data } = props;
    return (
        <>
            <div>
                <p>test3</p>
                <p>text: {data.text}</p>
            </div>
        </>
    );
}
```

- さっきの例ではpage側で使っていた `ApiComponent` をここで使う。基本的にはpageのほうでやっていた実装例と同じ。つまり、pageに返すより前に、component側でpageに返す情報を先んじて作っておく。pageは単にそれを利用するだけ。
- この例では「複数のAPI」として3つの異なるAPIを呼び出す想定。`export const Test1 = () => { ...` の流れの3つがそれに相当する。やってることは `/api/test?text=test1` と、先ほどのAPIにリクエストパラメータ付きで呼び出して、結果を加工して編集してもらって（ただし編集定義はここに定義している `const TestContent1: React.FC<Props> = (props) => { ...` を渡す）返す。そんだけ。
- `const TestContent1: React.FC<Props> = (props) => { ...` の部分はほとんど変わり映えしてなくて、中身の `<p>test1</p>` とかの部分だけが違っているだけ。これは「複数のAPIが（一応）それぞれに応じた編集定義をもってページ情報を生成している」ことを確認したかっただけの意図しかない（ここの、文字列部分で違いを見分けるため）実際にはもっと凝ることになると思う。。

## APIの実行指示と実行結果の画面編集を行うpage

```typescript:src/pages/testx.tsx
import { Test1 , Test2, Test3 } from '../components/test';
import type { NextPage } from 'next';

const TestX:NextPage = () => {
    return (
        <>
                {Test1()}
                {Test2()}
                {Test3()}
        </>
    );
};

export default TestX;
```

- これは滅茶苦茶簡素である。既にやるべきことは上の２つで終わっているからだ。このページはそれを呼び出す役割しか持っていない。呼び出し方はimportして `{Test1()}`とかするだけ。これだけで1ページからTest1～Test3の3つのAPIを呼び出す画面の出来上がりだ。


# 環境

- RemoteContainerで開発。ベースイメージは`node:16-bullseye-slim`
- "next": "12.0.10",
- "react": "17.0.2",
- "react-dom": "17.0.2",
- "swr": "^1.2.2"