はじめに
headlessCMSを叩くとき、もっと言えばAPIを叩くときの一番の敵は型です。
当然といえば当然なのですが、axiosやfetchなんかでAPIを叩いたとき、型は我々を守ってくれません。APIを叩くまでデータ構造が予想できないためです。
これが自力で実装したAPIなんかだとそこまで苦ではないのですが、headlessCMSの場合自力で実装していないのでふんわりとしか構造がわかりません。
つらいです。
つらいので、これを解決していこうと思います。
型情報をいかに取得するか
今回はOpenAPIを利用します。
FastAPIなんかからOpenAPIドキュメントを生成できるのは有名かと思いますが、これと同じようにStrapiからOpenAPIを取得します。
OpenAPIには型情報が含まれているので、ここから型を生成することを目指します。
StrapiからOpenAPIを生成する
公式がプラグインを作ってくれているので、これを使用します。
yarn add @strapi/plugin-documentation
インストールコマンドを実行後にdevelopでサーバーを起動すると、自動で以下のパスにOpenAPIファイルが生成されます。
{
"openapi": "3.0.0",
"info": {
"version": "1.0.0",
"title": "DOCUMENTATION",
"description": "",
"termsOfService": "YOUR_TERMS_OF_SERVICE_URL",
"contact": {
"name": "TEAM",
"email": "contact-email@something.io",
"url": "mywebsite.io"
},
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
"x-generation-date": "2022-12-19T11:55:47.857Z"
},
"x-strapi-config": {
"path": "/documentation",
"showGeneratedFiles": true,
"generateDefaultResponse": true,
"plugins": [
"email",
"upload",
"users-permissions"
]
},
"servers": [
{
"url": "http://localhost:1337/api",
"description": "Development server"
}
],
// 中略
}
ちなみに、一つしかContent-Tyoe Builderでスキーマを足してないにも関わらず約18000行ありました。こんなの手で作る気にならんわ。
いやーすごい、普段の自分より明らかにもっと書き込んでる。
これなら安心して型の生成元として扱えそうですね。
OpenAPI -> types/schema.ts
変換には、openapi-typescript
というライブラリを使用します。
今回はjsonのOpenAPIですが、よく見るyaml形式にも対応しています。
cli上でnpxで実行できます。
npx openapi-typescript <openapi-file-path> --output <file-name>
# ex) npx openapi-typescript ../<strapi-project-name>/src/extensions/documentation/documentation/1.0.0/full_documentation.json --output schema.ts
量が量なので時間かかるかなーと思いましたが、38msくらいでサクっと終わりました。
これを、例えばNext.jsのpagesで使用するとこうなります。
import type {
GetStaticPaths,
GetStaticPropsContext,
InferGetStaticPropsType,
NextPage,
} from "next";
import axios from "axios";
import { paths } from "schema";
import { ParsedUrlQuery } from "node:querystring";
const Home: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = (
props
) => {
return (
<div className="p-16 whitespace-pre-wrap">
<p>{JSON.stringify(props?.attributes, null, 2)}</p>
</div>
);
};
interface Params extends ParsedUrlQuery {
pageId: string;
}
export const getStaticPaths: GetStaticPaths<Params> = async () => {
const url = `articles`;
const res = await axios<
paths["/articles"]["get"]["responses"][200]["content"]["application/json"]
>(
`${process.env.STRAPI_URL}/api/${url}?publicationState=preview&fields[0]=title`,
{
headers: {
Authorization: `Bearer ${process.env.Strapi_API}`,
},
}
);
if (!res.data.data) return { paths: [], fallback: false };
const pageIds: { params: Params }[] = res.data.data
.filter(({ id }) => id !== undefined)
.map((item) => {
return { params: { pageId: `${item.id}` } };
});
return {
paths: pageIds,
fallback: false,
};
};
export const getStaticProps = async ({
params,
}: GetStaticPropsContext<Params>) => {
if (!params) return { props: {} };
const pageId = params.pageId;
const url = `${process.env.STRAPI_URL}/api/articles/${pageId}?publicationState=preview`;
const res = await axios<
paths[`/articles/{id}`]["get"]["responses"][200]["content"]["application/json"]
>(url, {
headers: {
Authorization: `Bearer ${process.env.Strapi_API}`,
},
});
const article = res.data.data;
return { props: article };
};
export default Home;
少し歪ですが、axios<paths["/articles"]["get"]["responses"][200]["content"]["application/json"]>
のように返ってくる値の型を付けることができます。
ちなみに表示内容はこんな感じです。
ただJSON.stringfyして吐かせただけなので味気ないですが、狙った通りの値が取得できています。