SmartHR Tech Blog

スマートエイチアール開発者ブログ

OpenAPI Generator と TypeScript で型安全にフロントエンド開発をしている話

こんにちは、SmartHR でフロントエンド開発を担当している @Tokky0425 です。

この記事では、私のプロダクトでの OpenAPI Generator を使ったフロントエンド開発の取り組みを紹介していきます。

目次

OpenAPI とは

OpenAPI とは、「REST API のドキュメントの記述形式を定めた仕様」のことを指しています。

簡単な例ですが、下記のような YAML ファイルがあるとします。

schema.yml

paths:
  "/some_path":
    get:
      responses:
        '200':
          description: 'OK'
          content:
            application/json:
              schema:
                type: object
                properties:
                  someKey:
                    type: string

このような記述をしたときに、

  • /some_path というパスに
  • GET リクエストを送ると
  • JSON 形式でデータが返ってきて
  • その JSON は soemKey キーを持っていて
  • 値は string 型である

という解釈になるよ、というのが、OpenAPI としての「仕様」ということになります。

こんな感じの OpenAPI の仕様に沿って書かれたドキュメントを用意しておくと、OpenAPI に対応したツールと連携してあれやこれやができるということですね。スキーマファイルの形式としては、YAML だけでなく JSON でも有効です。

ちなみに OpenAPI は以前は Swagger という名前だったのですが、今でも混同して使われている場面をよく見かけます。 現状の使い分けとしては、

  • OpenAPI: 仕様そのもののこと
  • Swagger: OpenAPI の仕様に則って何かをするツール郡につけられている名前のこと

という理解が正しいようです。

かつては仕様も含めて Swagger と呼ばれていたのですが、仕様に関してはベンダーに対して中立的であるのが望ましいということで OpenAPI という名前になり、一方でツール郡については引き続き Swagger というブランドを使っていく、という経緯があり、今の関係性に落ち着いているようです。

「ラクラク分析レポート」の DX 上の課題

さて、SmartHR のプロダクトの話に移りましょう。

私が担当している「ラクラク分析レポート」というプロダクトは、バックエンドが Ruby on Rails、フロントエンドが React/TypeScript で動いています。

「ラクラク分析レポート」では、開発の初期から OpenAPI のドキュメントはきちんと作られていて、 committee という gem によって、Rails が API のスキーマから逸脱したデータを送信しようとした際にはエラーを吐くようにする、という取り組みをしていました。 これによって、スキーマを書いてから API を実装するという、スキーマ駆動的な開発ができていたわけです。

ところが、これで API にまつわる全ての問題が解決されたかというと、そういうわけではありませんでした。

というのも、いつからか「バックエンドではスキーマに則った開発ができているけど、フロント側で想定しているデータ型と齟齬が出ている」という問題が出てきてしまったのです。

どんなにバックエンドで仕様通りの完璧な API を実装したとしても、フロントエンドでの実装がそれから乖離してしてしまっていては元も子もありません。

そこで「ラクラク分析レポート」チームでは、OpenAPI Generator を導入してこの問題の解決を試みました。

OpenAPI Generator とは

OpenAPI Generator とは、その名の通り「OpenAPI で書かれたドキュメントから、クライアントコードを自動で生成してくれる便利 OSS」です。 https://github.com/OpenAPITools/openapi-generator

生成できる言語は全部で 34 言語と、かなり広範囲をカバーしています。 (ただし、基本的にメンテナンスは有志で行われているので、実装状況にはばらつきがあります。)

Mac ユーザーであれば Homebew 経由で CLI がダウンロードできたり、npm パッケージでも配布されていたりもするのですが、いずれの場合も Java のランタイムが必要になります。 できるだけ個別の環境に依存したくないということであれば、配布されている Docker イメージを使って CLI を実行するのが良いでしょう。 この記事内でも Docker イメージを使ったケースの紹介をしています。

実際に generate してみる

私が担当している「ラクラク分析レポート」の開発における状況をもう一度整理しておくと、

  • 解決したい課題
    • バックエンドではスキーマに沿った実装をしているが、フロントエンドはスキーマを見る仕組みがないため、実装の乖離が発生してしまっている
  • 解決へのアプローチ
    • スキーマに沿ったフロントエンド用のクライアントコードを用意する

ということになります。

フロントエンド用のコードということで、今回は axios を使った TypeScript のコードを自動生成していくことにします。 環境に依存しないように、Docker イメージを使って OpenAPI Generator を実行していきましょう。

実行コードを書くと、このような形になります。

openapi-generate.sh

docker run --rm -v ${PWD}:/local \
    openapitools/openapi-generator-cli:v4.2.2 generate \
    -i /local/docs/schema.yml \
    -c /local/openapi-generator-config.yml \
    -g typescript-axios \
    -o /local/client/src/api/generated

ここでやっているのは、

  • docs/schema.yml のファイルを元に
  • openapi-generator-config.yml の設定に従って
  • axios を使った TypeScript 形式で
  • /client/src/api/generated ディレクトリ内に生成ファイルを吐き出す

ということです。

schema.yml はこうなっているとします。 少々記述は多いですが、/api/books/{book_id} を叩くと、title キーに文字列の入った JSON が返ってくるだけの API です。

openapi: 3.0.0
info:
  title: sample
  version: '1.0'
servers:
  - url: http://localhost:3000
paths:
  "/api/books/{book_id}":
    get:
      operationId: fetchBook
      parameters:
        - name: book_id
          in: path
          schema:
            type: string
          required: true
          style: simple
          explode: false
      responses:
        '200':
          description: 'OK'
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                required:
                  - title

この状態でシェルを実行すると、/client/src/api/generated に下記のようなファイルが生成されます。

generated
  ├── api.ts
  ├── base.ts
  ├── configuration.ts
  ├── git_push.sh
  └── index.ts

生成ファイルを使ってみる

api.ts の中を見てみると、下記の3つのインターフェースが自動で作られています。

  • DefaultApiFp: 関数型プログラミングで使う
  • DefaultApiFactory: ファクトリーパターンとして使う
  • DefaultApi: オブジェクト指向プログラミングで使う

どれも使用感的には大差ないのですが、今回は、DefaultApi を利用することにします。

上記の YAML ファイルの例だと、下記のように使用することになります。

const api = new DefaultApi()

;(async () => {
  const book = await api.fetchBook('some-id')
})()

ここの fetchBook というメソッド名は、schema.yml 上の operationId というフィールドの値に指定されたものです。 (operationId の指定がないと、「パス名 + HTTP メソッド名」のようなものが自動で割り当てられることになります。)

型情報を出力してみる

上記の YAML ファイルだと、実は返り値の型情報があまり綺麗ではありません。

f:id:tokky0425:20200824141815p:plain

返り値の型が InlineResponse200 という自動でつけられた名前になっていますが、きちんと名前をつけて出力したいところです。

これは、OpenAPI の components/schema を使ってスキーマを記述することで実現できます。

# 略
paths:
  "/api/books/{book_id}":
    get:
      # 略
      responses:
        '200':
          description: 'OK'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/book' # book の情報をローカル参照するように変更
components:
  schemas:
    book:
      type: object
      properties:
        title:
          type: string
      required:
        - title

f:id:tokky0425:20200824142159p:plain

Book という名前で型が出力できました。 当然、この型はアプリケーション内で使い回すこともできます。

OpenAPI Generator で TypeScript のコードを生成する場合は、型システムの恩恵を最大限に受けるために、小さなリソースでもできるだけ components/schema に切り出していくのが良いでしょう。

組み込み・運用の工夫

次に、実際にプロジェクトに組み込んで運用していくときのことについてお話しようと思います。

chokidar で監視する

フロントエンド開発では、Webpack などを使ってファイルの監視・ビルドを自動で行うというのが基本的なスタイルかと思いますが、スキーマファイルに関しても同じく「スキーマファイルに変更があったら OpenAPI Generator を実行する」といったことをしたくなります。

これには chokidar という npm モジュールが便利です。 chokidar CLI を利用すると、

chokidar \"監視したいファイル or ディレクトリ\" --command \"変更を検知したときの実行コマンド\"

という風に、簡単にファイル監視処理を npm scripts 上で行えるようになります。

実際の「ラクラク分析レポート」内の npm scripts は、下記のようになっています。(一部抜粋)

{
  "scripts": {
    "start": "run-p start:*",
    "start:app": "npm run generate-api && webpack-dev-server",
    "start:watch-schema": "chokidar \"docs/schema.json/\" --command \"npm run generate-api\"",
    "generate-api": "sh openapi-generate.sh"
  }
}

npm start を実行すると、 start:app と並行して start:watch-schema も実行され、docs/schema.json の監視も始まるというわけですね。

(余談ですが、私のチームではエコシステム上都合が良かった JSON 形式でスキーマファイルを管理しています。)

lint-staged に組み込む

chokidar を使ったファイル変更の監視だけだと、npm scripts を実行していない状態でスキーマファイルを変更した場合、OpenAPI Generator が実行されていない状態で git に変更ファイルをコミットできてしまいます。

これはつまり、TypeScript 上で発生するべき型エラーを無視した状態のコードをコミットできてしてしまうということになります。

この問題には huskylint-staged という npm モジュールを使うことで対応することができます。

husky は git のイベントをトリガーにして何かしらの処理を実行できるようにするモジュールで、ling-staged は git のステージ上にあるファイルに対して何かしらの処理を実行できるようにするモジュールです。 どちらも ESLint などと組み合わせて使われることが多いですね。

「ラクラク分析レポート」では、package.json 上に下記のような設定をしています。(一部抜粋)

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "docs/schema.json": [
      "npm run generate-api",
      "git add client/src/api/generated"
    ]
  },
}

この設定をすることで、

  • git のコミットが行われたとき、
  • ステージ上に docs/schema.json があれば、
  • npm run generate-api を実行し、
  • git add client/src/api/generated を実行する
  • その後コミットが実行される

という挙動にすることができます。

なお、そもそも OpenAPI Generator による生成ファイルは git 管理せずにビルドパイプライン上で生成する、という開発フローにする場合は当然このような対応は不要になります。

メリット・デメリット

半年ほど OpenAPI Generator の運用をしてみて、当初持っていた課題に対する解決のアプローチとしては機能している実感はあるのですが、一方でデメリットも顕在化してきました。

メリット

  • バックエンドからのレスポンスを疑う必要がなくなる
  • スキーマファイルがきれいになる

メリットの1つ目は、「バックエンドからの API のレスポンスを疑う必要がなくなる」ということです。

そもそもこれこそが解決したい課題だったので、当然といえば当然なのですが、レスポンスの形が型情報付きで保証されているという安心感は大きく、何かしら意図しない挙動があったときも、レスポンスそのものの内容を疑う必要がなくなりました。

メリットの2つ目は、「スキーマファイルがきれいになる」というものです。 これは完全に副次的なメリットです。

例えば、OpenAPI Generator できちんと型情報を出そうとすると、スキーマファイル上では components セクションを適切に扱って、型情報として切り出しが行われる単位に分割しなければなりません。 他にも、つい疎かになりがちな required 属性もきちんと記載するようにしなければならなかったり、適切なメソッド名にするために operationId を降る必要があったり、等々。

このように、OpenAPI Generator でコードを生成しようとすると、結果的にスキーマファイルも正しく書いていかなければならなくなるので、自然とスキーマファイル自体が整理されていく、といった効果がありました。

デメリット

  • スキーマを媒介に、クライアントとサーバーのコードが密結合になる

デメリットとしては、「スキーマを媒介に、クライアントとサーバーのコードが密結合になる」というものがあります。

API を変更しようとしてスキーマファイルに変更を加えると、OpenAPI Generator によって新しいコードが吐き出され、フロントエンドのコードのコンパイルが通らなくなるので、そちらも併せて修正する必要が出てきます。

つまり、バックエンドとフロントエンドが完全に分業してやっているような状態だと、「バックエンドの作業として、とりあえずスキーマと API だけ変更してプルリクを出す」のような進め方ができなくなってしまうということです。

スキーマに対して密結合になるのは、実装の正しさを保証する意味では基本的には良いことなのですが、作業の進め方に関しては、チームによっては話し合いが必要になってくるかもしれません。

まとめ

  • OpenAPI スキーマのドキュメントは、それ単体ではスキーマに沿った実装を保証することはできない
  • そこで、OpenAPI Generator を使えばスキーマに沿った API のクライアントコードを自動生成することができて便利
  • フロントエンド開発に組み込む場合は、npm scripts 上にタスクを組み込んでおくとさらに便利
  • SmartHR では、プロダクトを一緒に開発してくれるエンジニアを大大大募集中
  • Reat/TypeScript、Ruby on Rails でのプロダクト開発に興味がある人、もしくは OpenAPI の運用・活用に一家言ある人は下記のリンクをクリック