READYFORのフロントエンジニア

菅原弘太郎氏(以下、菅原):それでは「OpenAPI GeneratorとTypeScriptによる型安全なスキーマ駆動開発」と題して、発表します。自己紹介します。2020年11月に、フロントエンドエンジニアとしてREADYFORに入社しました。岩手県在住で、フルリモートで勤務しています。ReactとTypeScriptが好きで、React Hook Formのメンバーなので、もしフォローしてくれる方がいれば、フォローしてください。

フロントエンドとバックエンドの分離

まずはフロントエンドとバックエンドの分離について話します。READYFORのサービスは、さまざまドメインが入り組んだ、1つのモノリスなサービスです。これがドメインごとに分離されたサービスを目指しています。

現状、フロントエンドとバックエンドの分離フェーズで、フロントエンドはTypeScriptとNext.jsを用いたSPA、バックエンドはREST APIという構成になっています。

私はちょうどフロントエンドとバックエンドの分離フェーズ中に入社しました。スキーマ駆動開発を実施している話は聞いていましたが、「はたして何だろう」という状態でした。

自分の3ヶ月前と同じような方がいるかもしれないので、今日は、スキーマ駆動開発とは何か、どんなメリットがあるのか、スキーマ駆動開発を実践する上でどんなツールを活用しているのか。3ヶ月、スキーマ駆動開発を実践してみた所感などを話せればと思います。これからスキーマ駆動開発を実践したい方の参考になれば幸いです。

スキーマ駆動開発とはなにか

まずはスキーマ駆動開発ついて説明します。SPAとAPI開発において、さまざまな悩みごとがあると思います。例えば、API仕様書のフォーマットが人によってバラバラだったり、仕様書と実装の乖離が発生したり。APIの実装を待たないとフロントの開発に着手できなかったり、あとはフロント側で想定していたリクエストやレスポンスが返ってこなかったりということがあるかと思います。

ここで、スキーマ駆動開発の出番です。まずスキーマ駆動開発のスキーマについて説明します。スキーマとは、ある定められた形式で記述したものです。Web APIの仕様定義と思ってもらって問題ありません。エンドポイントや、リクエストやレスポンスのデータ構造、値の型やフォーマットなどを定義したものを指します。

では、スキーマ駆動開発がどんなものかというと、Web APIの開発者と利用者、いわゆるバックエンドエンジニアとフロントエンドエンジニアでスキーマ設計を行い、スキーマを中心としたスキーマファーストな開発を行うことです。

これを実現するためには、さまざまなツールを活用する必要がありますが、スキーマが中心になって、ドキュメントの生成やコードの生成、モックサーバーの起動ができるようになります。

続いて、開発フローの違いについてです。スキーマ駆動開発以前では、APIが実装されてからフロントの実装に入るパターンがほとんどかと思います。これはフロント側でモックデータなどを用意すれば一応解決はできますがそもそも用意するのが面倒だったり手間がかかったりします。

これがスキーマ駆動開発を実践することでどうなるかというと、フロントとバックエンドでスキーマの設計を行って、コード生成ツールやモックサーバーを活用することで、APIの実装を待たずにフロント側で実装に入れます。

READYFORで活用しているスキーマ定義のツール

それでは、READYFORで活用しているツールの紹介です。まずはスキーマ定義から見ていきます。

知っている方も多いとは思いますが、READYFORではOpenAPIでスキーマを定義しています。Swaggerと聞けば知っている方がほとんどだと思うのですが、これはWeb APIを記述するためのフォーマットで、JSONやYAML形式での記述が可能です。READYFORではOpenAPI 3.0を使用しています。

イメージ的にはこのような感じになっています。左側がOpenAPIで定義したスキーマですが、「string型のpetIdのパラメータを、petsエンドポイントに対してGETリクエストすることによって、id・name・tagを含んだJSON形式で200レスポンスするよ」という意味合いになります。

スキーマ定義のやり方

続いてスキーマ定義の仕方ですが、ほとんどの場合はSwagger Editorを用いてスキーマを定義するのが一般的かと思います。これはどんなものかというと、左側でスキーマを編集しながら右側でプレビュー。いわゆる左側でスキーマを定義したものが、リアルタイムでAPIドキュメントとして可視化されて見れるエディタになっています。

また、ほかの選択肢としてStoplight Studioというものがあります。Swaggerとは違って、GUIです。そのため、OpenAPIのスキーマ定義についてあまり詳しくない人でも、ポチポチGUIをいじるだけでスキーマを定義できます。

Swagger Editorの画像は、このようなイメージです。

Stoplight Studioはこんな感じです。実際に触ってみたほうがいいと思います。

ドキュメント自動生成ツールとコード生成

続いて、ドキュメントの自動生成ツールについての説明をします。これも知っている方がほとんどかと思いますが、Swagger UIを活用しています。先ほど見せたSwagger Editorのプレビュー側だと思ってもらって問題ありません。スキーマからドキュメントが生成されるので、信頼できます。

続いて、コード生成について見ていきたいと思います。READYFORでは、コード自動生成ツールにOpenAPI Generatorを使用しています。RubyやTypeScriptをサポートしていて、ほかにもJavaやPHPなどもサポートしています。

今回はDockerの例ですが、左の意味としては「openapi.ymlファイルをもとに、src/types/apiディレクトリに生成ファイルを出力する」というコマンドになります。ツリーを見てもらえばわかると思いますが、このように、APIクライアントやモデルが作成されます。

READYFORでは、ジェネレーターとして「typescript-fetch」を使用しています。

実際に生成されたファイルはこんな感じです。APIクライアントや、あとはリクエスト・レスポンスの型が生成されます。

続いて、スキーマから自動で型が生成されるので、リクエストやレスポンスの型にミスがない。いわゆる自前で定義すると、JSONのキーに間違いがあったりなどすると思いますが、そういうのがなくなって、型を信頼できます。

あとは、APIクライアントなどのコードが生成されるので、定型コードを記述するコストも減るんじゃないかなと思います。また、スキーマが変更された際もビルドエラーで修正箇所を把握できるので、おすすめです。

スキーマ定義の3つのハマりどころ

続いて、ハマりどころを紹介したいと思います。まず1点目としては、tagsを指定せずにスキーマを定義すると、APIクラス名が「DefaultApi」 という名前で生成されてしまいます。これを回避するには、ちゃんとtagsを指定しておいたほうがよいです。

続いてハマりどころ2なんですが、operationIdを指定せずにスキーマを定義すると、APIクラスのメソッド名やリクエストデータの型名が自動で生成されてしまいます。これもoperationIdを付与することによって、自分の意図した命名ができるようになります。

ハマりどころ3としては、components/schemasを使用せずにスキーマを定義すると、レスポンスデータの型名というのも自動で生成されるため、「InlineResponseXXX」という感じで、けっこうわかりづらい感じに生成されてしまいます。これも、ちゃんとcomponent/schemaで区切って生成することで、意図した型名で生成できるようになります。

モックサーバーの活用とメリット

続いて、モックサーバーの活用についてです。モックサーバーを活用することで、APIの実装を待たずにフロント側で開発に着手できるようになります。READYFORでは、Prismというモックサーバーを使用しています。先ほど紹介したStoplight Studioにも、モックサーバーとして統合されています。

コマンドとしてはこんな感じです。openapi.yaml、スキーマをもとにモックサーバーを起動できます。デフォルトでは、毎回同じデータを返すスタティックレスポンスを生成します。一応-dフラグというものがあり、--dynamicオプションを付与することで、毎回異なるデータをレスポンスすることも可能です。

Prismの強力な機能の1つとして、Preferヘッダーを渡すことでレスポンスの変更が可能になります。例えば、コードに対して404のようなステータスコードを付与してあげると、404でレスポンスできるようになるし、さらにスキーマで定義したexampleを指定することによって、そのexampleをレスポンスできるようになります。

あとはdynamicというのがあり、先ほどCLIで紹介した-dオプションと同じダイナミックレスポンスを有効化する機能も、ヘッダーに付与できるようになります。

あとはなにより、Cypressと相性が非常によいです。例えば「入力フォームのsubmitボタンを押して422エラーがレスポンスされたときに、エラーメッセージが表示されるか?」といったテストが簡単にできます。

先ほど紹介したPreferヘッダーを使用してあげることで、これを実現できます。Cypressではリクエストヘッダーをカスマイズできるので、APIにリクエストする前に、ヘッダーをカスタマイズしてリクエストを投げることによって、このエラーレスポンスを返せるようになる、という感じです。

スキーマ駆動開発でよかったこと、つらかったこと

続いて所感についてですが、まずよかったことから説明したいと思います。もうすでに説明していると思いますが、スキーマからAPIドキュメントが生成されるので信頼できます。これはスキーマがメンテナンスされている前提ですが、スキーマファーストに開発していれば信頼できるものになります。

また、TypeScriptのコードが生成されるため、型安全に開発できます。あとは、モックサーバーがかなり強力なので、効率的・テスタブルな開発が可能です。さらに、PR上でOpenAPIのスキーマをベースにバックエンドと認識を合わせられるので、非常に効率的でやりやすいです。

いいところばかりでなくつらいところもあって。これはスキーマ駆動開発というよりは、openapi-generator-cliの問題ですが。CLIによって生成されたコードで、型エラーが発生する場合があります。oneOfやallOfを使用すると、けっこう発生してしまいます。

あえてジェネレータで-gオプションを指定して「typescript-fetch」から「typescript-axios」に変更して試してみました。そうすると、正しいコードが生成できたりするので、改善の余地はありそうだと思っています。

続いて、生成される型が最善ではないところです。enumは、Enum型で出力されます。これは当たり前といえば当たり前なんですが、できればUnion型に出力するオプションが欲しいです。また、nullableなenumをスキーマで定義しても、出力される型としてはnullableになりません。

あとはdate/date-timeフォーマットで指定したものがDate型で出力されます。本来であればstring型にしてほしいところですが、typescript-fetchのみ、そういう感じになってしまいます。

一応--type-mappingsオプションで型をマッピングできるので、これでstring型にはできます。しかし、型が修正されてもAPIクラス側で型エラーが発生するので、ここはフォーマットを指定しない手段をとるしかないかなと思っています。

スキーマ駆動開発の2つの検討事項

続いて、スキーマ駆動開発を実践した上で「こういうのを検討したほうがよさそう」ということを記載しています。

まず1点目が「OpenAPI Generatorによって生成されたAPIクライアントを使用するか、それとも型定義のみを使用するか?」です。生成されたAPIクライアントを使うのは非常に効率的ですが、当然それに依存することになります。フロントエンド 側の設計に対応できるかどうか、コードをちゃんと読んで、判断した上で使用するほうがよいかと思います。

続いて、ジェネレータをどれにするかの問題です。そのままtypescript-axiosを使っているならaxiosでもいいとは思いますが、型定義のみ使用する場合は、先に説明した出力される型定義との兼ね合いを見て決定したほうがよさそうだと感じています。

スキーマ駆動開発を実践して重要だと感じたこと

続いて、スキーマ駆動開発を実践してみて重要と感じたことです。これはもうスキーマファーストな上で最も重要だと思いますが、フロントエンドとバックエンドともに、スキーマからコードを生成しましょう、というところです。

仮にですが、フロント側のみスキーマからコードを生成しても、スキーマとAPIの実装に乖離が発生する可能性が高いです。最悪の場合、スキーマがメンテナンスされなくなってしまうので、避けたほうがよいでしょう。

いろいろ紹介しましたが、スキーマ駆動開発は、メリットがデメリットを遥かに上回ると思っています。興味をもった方は、ぜひ実践してみてください。ご清聴、ありがとうございました。