FJCT Tech blog

富士通クラウドテクノロジーズ公式エンジニアブログです

富士通クラウドテクノロジーズ

FJCT/Tech blog

【CI戦術編 その9】自動生成しか勝たん openapi-typescript

ネットワークサービス部のid:a8544です。

連載10回目の今回は、OpenAPI Specification (OAS) から、TypeScriptのクライアントコードを生成するツール、 openapi-typescript を取り上げます。

OASを生成する意義やその方法は、前回の記事でご紹介しました。 その中で「OASを元にしたAPIクライアントを生成するツール」が存在することもご説明しました。 わたしたちはバックエンドサービスだけでなく、そのためのWebフロントエンドもあわせて開発しています。 openapi-typescriptは、そのWebフロントエンドのためのAPIクライアントを自動で生成してくれるツールとして、 わたしたちの開発において欠かせない重要な役割を果たしています。

本記事では、openapi-typescriptが開発の中でどのように活用されているのか、その方法と利点についてご説明します。

🤔 APIクライアントを自動生成する理由とその利点

APIクライアントは、APIへのリクエストを組み立て、またレスポンスを解釈するコードの集まりです。

APIクライアントは、APIで可能な操作をインタフェースとして表現しています。 そのため、どのようなレスポンスが返ってくるのか、リクエストはどのように組み立てるべきなのかを APIクライアントのコードを見るだけで理解できます。

型がインタフェースとして定義されていることで、 例えばコード補完やtypoの検出ができ、開発者体験が向上します(型がつくことの利点は、 Pythonの型ヒントについての第6回の記事でもご説明しています)。 また、TypeScript等の型が静的に決定することを要求する言語では事実上必須です。

APIクライアントは、APIのエンドポイント全てについてインタフェースを定義する必要がありますので、 手で書くには膨大な労力が必要です。そのため自動で生成する仕組みがopenapi-typescriptに限らず多数存在します。

自動生成にはさらに利点があります。CI等でバックエンドサービスのAPIに変更がある都度生成するようにすれば、 APIクライアントがバックエンドサービスの最新の実装から遅れることがなくなります。 APIクライアントを使うフロントエンドのテストやトランスパイル、ビルドも自動化していれば、 最新のバックエンドサービスの変更が、自動的にフロントエンドでもテストできるようになります。 これにより、変更に伴って動かなくなる箇所が、もちろん完全ではないにせよ、検知できるようになります。

🏭 openapi-typescriptによるAPIクライアント自動生成の方法

openapi-typescriptの使いかたは簡単です。まず、Yarn等のパッケージマネージャーでインストールします。

$ yarn add openapi-typescript -D
$

次に、OASの仕様に則って書かれたドキュメント(以下、単にOASといいます)を引数として与えて実行します。 OASの生成方法の例は、前回の記事でご紹介しています。

$ yarn run openapi-typescript ./api.json --output ./libs/api.ts
✨ openapi-typescript 6.2.1
🚀 ./api.json → libs/api.ts [25ms]
$

生成されるソースコードには、OASの定義に沿う形で記述された、各エンドポイント のリクエスト・レスポンスが十分表現できるような型の情報が含まれています。 ただし型の情報のみなので、実際にリクエストを処理する部分のコードは別途用意する必要があります。

簡単には、openapi-typescriptの公式のREADMEにもあるように、 fetch を使った結果に生成された型を当てることもできます。

import { paths } from './libs/api'

const endpoint = 'http://127.0.0.1:8000'

const getItem = async (itemId: number) => {
  const response: paths['/items/{item_id}']['put']['responses'][200]['content']['application/json'] =
    await fetch(`${endpoint}/items/${itemId}`, {
      method: 'PUT',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        name: 'newItem',
        price: 1000,
        tax: 1.2,
      }),
    }).then((r) => r.json())
  return response
}

const main = async () => {
  const item = await getItem(100)
  return item.item.name
}

main().then((text) => console.log(text))

上記例では、 getItem の返り値にはOASにもとづく型が付いていますので、補完が効きます。

エディタで編集中にプロパティの名前が補完される様子を示したスクリーンショット
プロパティの名前が補完されます

ただし、パスを含む長い型を自分で書いたり、適切な型を選択する手間がまだあります。 さらに、 fetch の引数、例えばリクエストボディには型がついていないなど、 いまひとつOASを活用できていません。

もう少し抽象化したい場合は、例えば同じREADMEで紹介されている openapi-typescript-fetch を使うと楽になります。

import { paths } from './libs/api'
import { Fetcher } from 'openapi-typescript-fetch'

const endpoint = 'http://127.0.0.1:8200'
const fetcher = Fetcher.for<paths>()
fetcher.configure({
  baseUrl: endpoint,
})

const updateItem = fetcher.path('/items/{item_id}').method('put').create()

// ここまでが準備

// 実際に使うときは…

const main = async () => {
  const { data } = await updateItem({
    item_id: 1,
    name: 'newItem',
    price: 100,
  })
  return data.item.name
}

main().then((text) => console.log(text))

このように、PUTリクエストを送るとか、パスはこうだとかいう情報が隠蔽され、ボディの組み立てもより直感的になりました。 型情報のおかげで、上記コードの末尾にある updateItem の呼び出しにおいて、 必要なパラメータ(item_id, name 等)についても型チェックが可能です。 もちろん、入力補完も有効です。

エディタで編集中に引数中のプロパティ名が補完される様子を示したスクリーンショット
引数にも名前が補完されます

👹 効用: 変更に強くなる

もしバックエンドサービス側でAPIの破壊的な変更があったら、クライアントの実装はどうなるでしょうか。 その際でもOASを更新し、APIクライアントを再度生成してさえいれば、 クライアント側で型のエラーとして検出できることがあります。

例えば、バックエンドサービス側の Item の定義から tax が消えたとしましょう。

class Item(BaseModel):
    name: str = Field(example="Foo")
    description: str | None = Field(default=None, example="A very nice Item")
    price: float = Field(example=35.4)
    # tax: float | None = Field(default=None, example=3.2)  # これがなくなる

このとき、 tax を更新するような下記のコードは、 tsc で型のエラーになります。 updateItem の定義はOASから抽出されていますから、引数の型も自動的に更新されるためです。

エディタで編集中にtscで発生したエラーが表示されている様子を示したスクリーンショット
tscにおいてエラーが発生したことに気付けます

単純なことではあるのですが、このエラーの検出が人間の介入なく完了できることに意義があります。 この検出までに必要な手順は下記の通りですが、いずれも単純なコマンドで実行できます。

  • OASの生成(FastAPIによるOAS生成機能等)
  • APIクライアントの生成 (openapi-typescript)
  • TypeScriptのコンパイル (tsc)

リポジトリにコミットされているAPIクライアントを常に最新に保つ

わたしたちは、OASに加え、それから生成されるAPIクライアントもリポジトリにコミットしています。 この場合は、前回同様に、OASが更新されたのにAPIクライアントは更新されていない、という問題が生じる可能性があります。 この問題に対処するため、CIで下記コマンドを実行するようにしています。

  • yarn run openapi-typescript ./api.json --output ./libs/api.ts
  • [ $(git status --porcelain ./libs | wc -l) -eq 0 ] || exit 1

やっていることは単純で、APIクライアントをCI上でも生成し、差分がないかどうかを検知しているだけです。

バックエンドサービスの実装とそのOASの差分がないことは、前回の記事でご紹介した方法により担保されています。 よって、バックエンドサービスの実装からAPIクライアントまで、全て同期している (そうでなければ、CIを通過しない)ことがこれで確認できるわけです。

さいごに

OASからAPIクライアントを生成し、利用する方法をご説明しました。

APIクライアントの生成は、 機械可読なOASという形式で仕様を定義することで得られる大きな利点の一つです。 単にコードを書く労力を削減できるだけでなく、変更によって生じる問題を機械的に検知できるようになり、 迅速にコードを変更していくワークフローの大きな助けになります。

次回は依存ライブラリのアップデートチェックツール、Renovateをご紹介する予定です。 お楽しみに。

連載バックナンバー