🏭

OpenAPI からなるべく生成するフロントエンド開発

2022/10/10に公開
1

こんにちは。
スキーマから何かを生成するのが大好きな seya と申します。

追記

今後は基本お手製スクリプトは書かないで ChatGPT にお願いした方がいいと思います。
https://twitter.com/sekikazu01/status/1637074325617659909

------------- 追記 終わり ------------

今の会社では REST API のスキーマを OpenAPI で記述しているのですが、それを活用して何かしらを生成するスクリプトをよく書いています。
そんなネタがそれなりに溜まってきたので一挙大放出しようというのがこの記事です。

具体的には次のようなものを作りました。

  • TypeScript の型と API の生成 by aspida
  • テストのための Factory 関数の生成
  • msw handlers の生成

API スキーマから生成のデメリット

まず初めに、"生成"というのはとても生産的に感じますが、デメリットも存在するので触れておきます。

一番大きなデメリットはずばり「生成したものは(基本)直接いじれない」ということです。

なぜなら継続的に使っていくことを想定すると、手動で書き換えてしまった際に再度生成ツールを使うと上書きされてしまうからです。
そうなると生成されたコードの機能が実運用に対して足りない時に、そのコードではなく生成する方のスクリプトをアップデートする必要があるわけです、つまりメンテ対象が増えると言えるでしょう。(OSS でメンテされているライブラリを利用する場合はその限りではないですが)

実際職場でお手製スクリプトを作っている実感として、ちょっと普段のプログラミングと違う感があったり、私が「一回動けばええやろ」とばかりに書き殴ってしまったせいか(ごめんね)他の人が触りづらいんじゃないかという雰囲気を感じます。

なので、こういった内部で作るスクリプトもしっかり他の人が読めるようにリファクタリングしたり、最悪メンテを続けるのがコスパに合わんとなった場合に手動アプデに切り替えられるような形にしておくのが大事なのかなと思います。

TypeScript の型と API の生成 by aspida

昨今 API の型はスキーマから生成することが多いと思います。
これは自作ではなく aspida というライブラリを使っています。

https://github.com/aspida/aspida

ちなみに作っている方は日本人の方です。応援しましょう。

https://twitter.com/m_mitsuhide/

aspida ではこんな感じのコンフィグを書いて

module.exports = {
  outputEachDir: true,
  baseURL: "https://localhost:8000",
  trailingSlash: true,
  openapi: { inputFile: "openapi.yml" },
};

openapi2aspida というものをインストールしコマンドを実行すれば

{
  "scripts": {
    "generate:aspida": "openapi2aspida",
  },
}

スキーマに応じた型や、API にリクエストする関数を生成してくれます。
生成されたものはシンプルに import して実行するだけです。型も api/@types/index.ts に全部書かれています。

import api from "@/api/me/$api";

api(apiClient)
    .get()
  .then((result) => {
    // ちゃんとスキーマに応じた型がついてて幸せだよ
  })
  .catch((e) => {
    ...
  });

SWR などにも対応しており、React の hooks とも組み合わせて使えたりします。

import useAspidaSWR from '@aspida/swr';

const SomeReactComponent = () => {
   const { data, error } = useAspidaSWR(api, "$get");
}

ちなみにこういった API スキーマから生成する系のツールとして、他には openapi-generator という老舗ライブラリがあるのですが、これは実行に Java が必要なため、これのためだけに Java をインストールするのは「いや〜」と感じたため aspida にしました。

あと私は後から知ったのですがこういうライブラリもあるみたいです。(Azure なので Microsoft 謹製っぽい)

https://github.com/Azure/autorest

テストのための Factory 関数の生成

先ほど型を一気に生成できるようにはなりましたが、次にテストを書いている時に「あ〜この Props に渡しているオブジェクトの型 API で定義しているそのまんまだしテストデータ一々手で書くのめんどくせぇな〜、そうだ、これも生成しちゃおう!」と思いました。

これは先ほど aspida で生み出された api/@types/index.ts を元に、TypeScript Compiler API を使うことによって実現できます。
そうして生まれたのが次のスクリプトです。

コードの中身(長いので閉じてます)

https://gist.github.com/kazuyaseki/cb00a0e01a36e4f5898b47000d08b739

これを実行すると次のような Factory 関数が生まれます。
これでテストや Storybook などで API レスポンスと同じ型のダミーデータを作る時に手間が多少減らせます。

export const me_userDefaultAttributes: me_user = {
  id: faker.datatype.string(),
  name: faker.datatype.string()
};

export const me_userFactory = (
  attirubutes?: Partial<me_user>
): me_user => ({
  ...me_userDefaultAttributes,
  ...attirubutes,
});

ただ現状だと何でもかんでもランダムな文字列を出していたりしてリアリスティックさに欠けるので、name とプロパティ名についていたら日本語名を出す email と入ってたらメルアド出す、など改善の余地は多々あるかな〜という感じです。

msw handlers の生成

テストで HTTP リクエストのモックでデファクト感の出てきた msw、下記のように API のパスと返す値をモックできます。

import { rest } from 'msw';
export const handlers = [
  rest.get('/api/me/', (req, res, ctx) => {
    return res(ctx.json({ name: "hoge", age: 29 }));
  }),
]

だがしかし、これ逐一書くのめんどくさいなぁといつものように感じ、パスも返すプロパティも全部 OpenAPI に定義されてるじゃ〜んと思ったのでこちらも生成するスクリプトを書いてみました。

コードの中身(長いので閉じてます)

https://gist.github.com/kazuyaseki/533326b97895f1000115959bd73546d4

テストではこんな感じで使います。
setupMockServer という関数は Takepepe さんのnext のテストのサンプルレポジトリから拝借してます。

const server = setupMockServer(...handlers);

describe(() => {
  test('何かしらのテスト', async () => {
      // テスト毎に特定の値を返したいときはこんな感じ
      server.use(
        rest.get('/hoge/fuga/', (req, res, ctx) => {
          return res(
            ctx.json({
              aaa: "hgoehogehoge"
            })
          );
        })
      );
   }
}

ただこれ実際に運用に使ってから思ったんですけど、テスト毎にちゃんと返したい値があることが多いので、結局テスト毎に handle の部分を書く羽目にはなるのですが、便利なコピペ元ができたと思えばまあ。

あとこれもついさっき読んで知ったのですが aspida の型と組み合わせる方法がよりエレガントなのでこっちの方がいいかもしれないです。

https://zenn.dev/takepepe/articles/typesafe-msw-with-aspida

全部を実行する Actions を作る

という訳で以上3つを紹介しました。
が、これを一々実行するのは面倒です。なのでなるべく自動化を試みます。

まずは全部を実行する npm script を定義します。

{
  "scripts": {
    "generate:aspida": "openapi2aspida && rm -rf ./src/api && mv -f api ./src",
    "generate:msw": "ts-node scripts/openapi-to-msw.ts",
    "generate:factories": "ts-node --esm scripts/generate-factories.ts",
    "generate:all": "npm run generate:aspida && npm run generate:msw && npm run generate:factories",
  },
}

そしてこれを実行する GitHub Actions を書きます。
...というのは以前別記事に書いたので下記をご参照ください。

https://zenn.dev/seya/articles/96ea2e7d81d0be

おわりに

OpenAPIのスキーマからもそうですし、そこで生成された型を元に更にTS Compiler APIを使うことによって いろんなコードを生成することができます。
繰り返し行われる記述が「これ全部スキーマにある情報やな〜」と感じる場合積極的に自動化を検討できると楽しくなると思います。

あとこれは若干自分語りなのですが、私は上記のような単純作業が苦手で「それをやるよりはスクリプトを書く方がクリエイティブで楽しい」という理由から書いています。

なので正直に申し上げますと、もしかしたら「全部を手作業で書く時間 < スクリプトを書く時間」の ROI が 1 を割っている可能性は十分にあります。
が、まあこれを一回作ることによって自分以外の人は単純作業を一切しなくなってハッピーになるし、且つこんな感じでパブリックに公開すれば更に多くの人の時間を削減することに貢献し、尚且つ自分も満足感高くてハッピーなことだと考えております。(可能であればちゃんと汎用性のあるライブラリにして OSS としてメンテできればモアベターなんですが)

こういう生成系の知見だったりライブラリが増えていって人類の無駄な手作業の時間がどんどん削減できていければなと思います。

というわけでぜひ TS Compiler API だったり aspida などの生成ツールを学んでみてください!