自分のところの/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としか表示しないので、実際にはlayoutcomponentとかと組み合わせて画面内の適切な箇所に適宜表示するように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"