自分のところの/api/
配下のAPIを実行してデータのやり取りをするアプリケーションにおいて、いろんなAPIを汎用的に呼び出せるようにしたうえで、フロント側の見せ方はページごとに変えたいと思っていた。
調べた感じズバッとこれだ!っていうのがなかったのだが、色々やった感じで以下の方法でうまく動作したので、メモとして残す。
単一APIの利用
テスト用のAPIを用意
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
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に関してはこちらの実装をほぼそのまま流用。全てのAPIの実行をこれに寄せることで、エラー発生時やロード中の表示を統一できる。
- APIの実行結果を加工と書いているが、加工処理の実態は引数で渡された
RF: React.FC<any>
に委ねられている。そしてこれはこのApiComponent
を呼び出す側から渡される形。 -
React.FC
の型を<any>
にしているのは、いろんなAPIの結果(いろんなオブジェクトの形)を受け取ることを意識して。統一できそうにないなと思ったので。統一の型があるならその型を指定する。
APIの実行指示と実行結果の画面編集を行うpage
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を用意するがリクエストパラメータを貰う以外はほぼ↑と同じ。
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
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
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;
- これは滅茶苦茶簡素である。既にやるべきことは上の2つで終わっているからだ。このページはそれを呼び出す役割しか持っていない。呼び出し方は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"