🙆

Next.js + microCMS の型周りどうしてるか暴露する

2022/05/09に公開

弊社コーポレートサイトは Next.js + TypeScript + microCMS で作られています。getStaticProps でデータフェッチするわけですが、microCMS から取得するデータの型定義を自動化したく試行錯誤したので私がやっているやり方をご紹介します。

まずはどんな手順なのか

  1. microCMS のスキーマをダウンロードする
  2. スキーマから型定義を生成する
  3. aspida の Methods を生成する
  4. aspida で axios の型定義済みメソッドを生成する

上記の順番で生成をします。とはいえ、3 についてはコマンドを叩けばできますし、2,4 については自動で生成されます。次に各手順を一つ一つ解説します。

ディレクトリ構成について

まずは前段階としてディレクトリの構成について解説をします。私の環境では Next.js のプロジェクトルートに src ディレクトリを作成し、その中に cmsディレクトリを配置して管理しています。
ディレクトリ

cms ディレクトリ配下の解説をします。

api ディレクトリ

aspida の型定義ファイルの生成物をここに格納します

schema ディレクトリ

microCMS のスキーマをここに格納します。

types ディレクトリ

スキーマから生成する各エンドポイントのレスポンス、リクエストパラメータの型定義をここに格納します。

utils ディレクトリ

実際に getStaticProps などで使うデータフェッチを行うメソッドをここで定義しています。

microCMS のスキーマをダウンロードする

API 設定 > API スキーマ > ページ下部の この設定をエクスポートする をクリックします。

API-スキーマ-microCMS

エクスポートした json データを見てみると下記のようになっています。

{
	"apiFields":[
		{
			"idValue":"MNNbwT-Gyt",
			"fieldId":"title",
			"name":"タイトル",
			"kind":"text",
			"required":true,
			"isUnique":false
		},
		{
			"fieldId":"image",
			"name":"アイキャッチ",
			"kind":"media"
		},
		{
			"fieldId":"body",
			"name":"本文",
			"kind":"repeater",
			"required":false,
			"customFieldCreatedAtList":[
				"2021-07-12T05:51:14.785Z",
				"2021-07-12T05:51:43.122Z"
			]
		},
		{
			"fieldId":"tags",
			"name":"タグ",
			"kind":"relationList"
		}
	],
	"customFields":[
		{
			"createdAt":"2021-07-12T05:51:14.785Z",
			"fieldId":"html",
			"name":"HTML",
			"fields":[
				{
					"idValue":"S_ECb4fI0Y",
					"fieldId":"content",
					"name":"HTML",
					"kind":"textArea"
				}
			],
			"position":[["S_ECb4fI0Y"]],
			"updatedAt":"2021-07-12T05:51:14.785Z",
			"viewerGroup":"LfC"
		},
		{
			"createdAt":"2021-07-12T05:51:43.122Z",
			"fieldId":"editor",
			"name":"エディター",
			"fields":[
				{
					"idValue":"5oQL7u3qSg",
					"fieldId":"content",
					"name":"Editor",
					"kind":"richEditor"
				}
			],
			"position":[["5oQL7u3qSg"]],
			"updatedAt":"2021-07-12T05:51:43.122Z",
			"viewerGroup":"LfC"
		}
	]
}

次に先ほど解説をしたディレクトリ構成のsrc/cms/schema に json データを入れます。

スキーマから型定義を生成する

microcms-typescript というライブラリを使ってスキーマから型定義を生成します。

https://www.npmjs.com/package/microcms-typescript

ライブラリをインストールします。

$ npm i microcms-typescript --save-dev

次に package.json の scripts に追記します。src/cms/schema をもとにsrc/cms/types/response.ts を生成します。

"scripts": {
		・・・
    "gen:types": "npx microcms-typescript src/cms/schema src/cms/types/response.ts"
		・・・
  },

これでレスポンスの型定義はできたのですが、リクエスト時の型定義ができていません。とは言っても microCMS のリクエストパラメータは全て同じでかつ決まったものなのでこれは手動で定義してしまいます。

src/cms/types/request.ts

export type ContentsQuery = {
  draftKey?: string
  offset?: number
  limit?: number
  orders?: string
  q?: string
  fields?: string
  ids?: string
  filters?: string
  depth?: number
}

export type ContentQuery = {
  draftKey?: string
  fields?: string
  depth?: number
}

aspida の Methods を生成する

aspida とは、axios などの HTTP クライアントにいい感じに型付けをしてくれるライブラリです。先ほどの request.ts と response.ts をもとに aspida を使って axios の型定義をしていきます。

ただ、aspida で使うそれぞれのエンドポイントのメソッドをまずは生成しなければいけません。例えば /blog であれば下記のようになります。

import { ContentsQuery, EndPoints } from 'src/cms/types'

export type Methods = {
  get: {
    query?: ContentsQuery
    resBody: EndPoints['gets']['blog']
  }
}

ほぼほぼ同じような定義で済むのでこれについてはコマンド一発で生成されるようにします。その際に scaffdog と言ってマークダウンファイルをテンプレートにコマンドで該当ファイルを生成してくれるライブラリ使うことで手間を省くことができます。

https://github.com/cats-oss/scaffdog

ただし microCMS ではオブジェクト形式と配列形式があるのでテンプレートをそれぞれ用意します。

$ npm i --save-dev scaffdog

プロジェクトルート直下で npx scaffdog init をすると 直下に /.scaffdog が生成されていると思うのでその中にマークダウンファイルを作成します。

api-array.md

---
name: "api-array"
root: "src/cms/apis"
output: "."
ignore: []
questions:
  value: "Please enter any text."
---

# `{{ inputs.value }}/index.ts`

```javascript
import { ContentsQuery, EndPoints } from 'src/cms/types'

export type Methods = {
  get: {
    query?: ContentsQuery
    resBody: EndPoints['gets']['{{ inputs.value }}']
  }
}
```

# `{{ inputs.value }}/_id@string/index.ts`

```javascript
import { ContentQuery, EndPoints } from 'src/cms/types'

export type Methods = {
  get: {
    query?: ContentQuery
    resBody: EndPoints['get']['{{ inputs.value }}']
  }
}
```

api-object.md

---
name: "api-object"
root: "src/cms/apis"
output: "."
ignore: []
questions:
  value: "Please enter any text."
---

# `{{ inputs.value }}/index.ts`

```javascript
import { ContentQuery, EndPoints } from 'src/cms/types'

export type Methods = {
  get: {
    query?: ContentQuery
    resBody: EndPoints['get']['{{ inputs.value }}']
  }
}
```

さらにコマンドで生成できるようにします。

package.json

"dog:arr": "npx scaffdog generate api-array",
"dog:obj": "npx scaffdog generate api-object",

aspida で axios の型定義済みメソッドを生成する

インストール

npm i --save @aspida/axios aspida axios

scripts に追加

"dev": "run-p dev:*",
"dev:client": "next --port 9000",
"dev:aspida": "aspida --watch",
"build": "next build",
"start": "next start",
"gen": "run-p gen:*",
"gen:types": "npx microcms-typescript src/cms/schema src/cms/types/response.ts",
"gen:aspida": "aspida",
"dog:arr": "npx scaffdog generate api-array",
"dog:obj": "npx scaffdog generate api-object",

--watch を付与することで監視をしてくれる。また、npm-run-all というパッケージで複数の scripts を同時に起動できます。

実際に生成してみる

下記コマンドを叩いて生成されるのを確認しましょう。

$ npm run dev

aspida の watch が起動したことを確認したら、microCMS のスキーマをダウンロードして schema ディレクトリに入れます。次に型定義ファイルを生成します。

$ npm run gen

さらに aspida の各エンドポイントごとのレスポンス、リクエスト定義をします。

$ npm run dog:arr
// or $ npm run dog:obj

これで aspida の watch が走っているので、自動で api ディレクトリに型定義ファイルが追加されていると思います。

ここまでが手順の解説です。では実際に使ってみます。とは言ってもまずは http クライアントの元を作らなければいけないので、先ほどのディレクトリの utils に index.ts を作ります。

import aspida from "@aspida/axios";
import axios from "axios";
import api from "../apis/$api";

const fetchConfig: Required<Parameters<typeof aspida>>[1] = {
  baseURL: `${process.env.API_BASE_URL}`,
  headers: { "X-MICROCMS-API-KEY": `${process.env.API_KEY}` },
};

export const client = api(aspida(axios, fetchConfig));

これで準備は全てできたので実際に使うとこのような感じになります。

export type BlogIndexProps = {
  info: EndPoints["get"]["info"];
  blog: EndPoints["gets"]["blog"];
};

export async function getStaticProps(): Promise<{ props: BlogIndexProps }> {
  const info = await client.info.get();
  const blog = await client.blog.get({ query: { limit: CONFIG.perPage } });

  return {
    props: {
      info: info.body,
      blog: blog.body,
    },
  };
}

export default IndexPage;

まとめ

いかがでしたでしょうか。生成した型定義をエンドポイントにも使えて、Component 側でも使えるのでめちゃくちゃ便利になりました。
もっと良い方法があれば乗り換えたいですが今のところはこれで満足してます笑

Discussion