🐕

Open API × Rails × TypeScriptでのスキーマ駆動開発|Offers Tech Blog

2022/04/11に公開約9,500字2件のコメント


プロダクト開発人材の複業転職プラットフォーム Offers を開発している、株式会社 overflow にて EM をやっております磯崎と申します。

日々プロダクトを開発している中で、様々な格闘があるかと思いますが、その中でも大分格闘してきた Open API を用いたスキーマ駆動開発について今回は書いてます。

この構成で運用してよかったと今のところは思ってますが、色々面倒な事や落とし穴にも直面してきました。自分たちの中に溜まっている知識を書き記していくのでどこかでお役に立てればハッピーです ☺️

最初に API を定義、その後開発を進めていくスキーマ駆動開発

そもそもスキーマ駆動開発とは、はじめに API を定義し、それを元にフロントエンド・バックエンドと開発を同時に進めていく開発フローです。

フロント実装においては通信部分で、「何を送信すべきか」、「何が返ってくるのか」を予め決まった状態で開発を進められるため、バックエンドの実装を待たずに、通信に関わる処理部分の実装も進めていくことができます。

一方で、バックエンドでは予め API を決定した状態で開発を進めることができるため、大きな変更や出戻りが発生しづらくなるメリットがあります。もちろん、開発していく中で変更すべき点が見えてくることも大いにありますが、その場合も、スキーマを修正するだけで、フロントエンドはバックエンドの変更を待つことなく、両者並行して開発をすすめることができます。

Open API とは API を定義するためのフォーマットとそれを取り巻くツール等を指す

そして、Open API はなんぞやといいますと、公式にこう書いてあります。

The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.

An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.

https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md

OpenAPI Initiative によって、OpenAPI Specification という、RESTful api の定義する上での共通のフォーマットが作られています。人間・機械にとっても共通で理解できる共通のフォーマットを作成して、どこでもだれでも読めるようにし、皆気持ちよく色々作ってこうぜってことですね。このフォーマットが統一されていることで、例えばスキーマからのコード自動生成や validation、様々な言語でのテストなどが行いやすくなる仕組みになっている訳です。

元々、Swagger Specification と呼ばれていましたが、OpenAPI Specification という名前に変更されました。Open API について調べていると Swagger に関しての情報も hit しますが、基本的には同じものとして捉えて OK です。

https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#appendix-a-revision-history

弊社での開発フロー

実際に弊社でのスキーマ駆動での開発を例としてご紹介します。
何も特別なことはしておらず、シンプルに API 定義 → それを元にフロントのコード自動生成 → バックエンドでの整合性チェックとしております。

1. API 定義

  1. データ設計含めて、基本的にはドメイン知識のあるバックエンドエンジニアが行うパターンが多いです。
  2. この段階で一度 PR を作成し、レビューします。

2. フロントエンドの実装開始

  1. 着手時に 1 で定義された API を確認した上で、なにか不明点や変更必要点があれば定義した方へ連絡し、確認します。
  2. 問題なければ実装を開始します。この時 Open Api Generator を使用して、フロントエンドのリクエストの処理を自動生成し、それを呼び出すことによって定義した API を Call します。自動生成されたコードを呼んでいるだけで、変更にも強く、より関心を分離させた状態で作ることができます。また、リクエスト対象の URL ミスなども発生しないため、そこに気を使わずに開発を進めることが可能です。

3. バックエンドの実装開始

  1. フロントエンドの実装開始と同時に着手開始可能です。
  2. 基本的には API 定義者がそのままバックエンド実装に入っていくパターンがほとんどです。API を設計していく段階で、なんとなく実装イメージもつかめてきているため、そのままやったほうがスムーズだからです。
  3. バックエンド実装では、1 で定義したスキーマの内容と乖離がないかどうかをテストに組み込みチェックをします。要は書いたものと実装内容が間違っていないかだけ気をつけます。

4. 結合テスト

  1. フロント、バックエンド両方わかる方が行ってそのまま直してしまうのがもちろん一番早いです。そうでない場合はどちらかがテスターになるなどして、不具合系を洗い出し、挙げられた項目を元に対応を切り出して、分割して対応していくことになります。

そして、検証環境に上げて、動作確認等をした後にリリースしていきます。

マルチレポでの運用が早々に限界きてやめました

ここでいうマルチレポ運用というのは、

  • API スキーマ
  • フロントエンド
  • バックエンド

と 3 つにわける、またはバックエンドに API スキーマを内包すると 2 つになる分け方でそれぞれ repository が存在する形です。

基本的にスキーマを元にフロントのコード自動生成、バックエンドでの整合性チェックを行うとなるとそれぞれ大本であるスキーマを参照する必要があります。

しかし、チーム開発していると複数同時開発が行われるため、それぞれブランチを参照するかを確認して切り替える作業が発生し、非常に面倒です。そしてこの場合は submodule を使って管理していくことになると思うのですが、この切替と運用が非常に面倒です。CI でテストを走らせる場合も、参照ブランチが違ってたら普通に予期せぬ結果になります。

そこで、素直に 1 つの repository にしてしまい、その時点での同一ブランチ内のスキーマを参照するやり方を取るほうが事故も減らせるし、何も考えずに運用できます。

余計なことには脳を使いたくない方針で、モノレポでいくのが今の所うまくいってます。

副業としてお手伝いしていただいている方も多いチーム構成には、良い感じにハマるのではないかという期待

そもそもなぜ Open API を用いたスキーマ駆動開発をやってみようとなったかといいますと、REST に慣れている開発者も多く、フロントエンド・バックエンドを分離した構成で開発していくにあたって、この構成がベストだと判断したからです。

スキーマの実装と実際の document が乖離するというあるあるも回避できるし、フロント側ではその document を元にリクエストの処理を自動生成してそれを呼ぶだけで通信が行えるため、開発スピードの安定性とスピード向上に寄与できると考えていました。GraphQL に慣れているメンバーが多いチーム構成でしたらそれも選択肢でしたが、採用時はそうではなかったため、プロダクトの初期開発スピードも踏まえた上で Open API を選択しました。

また、副業で多くの方に参画頂いており、様々な時間帯、タイムゾーンが違うメンバーも開発に携わっていただいております。その中で、スムーズに開発を進めていくためには非同期的なコミュニケーションの取り方・仕組みが重要になってきます。

その上で最初にガシッと API を定義してそこから開発を進めていくやり方というのは、フロントエンド ⇔ バックエンド ⇔ タスクオーナーのコミュニケーション量も減り、皆より集中して実装に取り組むことができます。

毎日同じ時間、気軽に連絡が取り合える働き方だと、お呼びして、お話して、さあ変更といけますが、副業の方と働く場合は必ずしもそうではないので、どのようなチーム構成になっても変化に対応できるようにしておくための一つの手段として試してみました。

また、無駄なコミュニケーションを取ること、自分の行動が何らかの要因によってブロックされることはストレスなので、そもそもそれを省けると言った点は非常に素晴らしいですね。

Stoplight を使って API を定義していくのが楽

Open API Specification に基づいて、API を書く際は、yaml or json で書いていくことになります。
もちろん慣れている場合は、淡々と文字列を記述していけばいいのですが、そうでない場合はかなりめんどくさいです。なので、GUI でポチポチ書いていけるものとして、stoplight studio を弊社では使用しています。

stoplight studioのエンドポイント設定画面
こんな感じで、定義していくことができるので、スピードがでます。

そして誰でもすぐに慣れて使うことができます。フロントエンド・バックエンド双方の開発者が設計部分以外で悩むことなくいじれるのはメリットですね。

mock server も速攻で立ち上がります

フロント単体で開発を進めていく中で、mock server があると便利ですね。
というかないと、ダミーデータを定義してそれで描画したり、実際に backend を動かして検証を進めていかなかればなりません。そのあたりのブロック要因を全て取っ払うために、定義されたスキーマを元に mock server を動かして、開発をしていくのがとてもスムーズです。

Prism を使えば、定義したスキーマを元に速攻で mock server を立ち上げて開発できます。node.js があれば動くので環境構築も楽ですし、docker を用意して立ち上げるような形をとっても良いですね。
また、オプションで動的な値を返してくれたり、リクエストのバリデーションも行うことができます。

mock server としての役目は充分で、これでフロントは通信部分まで一気に実装できます。実データとの結合をチェックしたい場合のみ、バックエンドを立ち上げて確認していく形でいくと、余計なことに時間取られずにスピーディーに開発できます。

また、stoplight.io で github 連携しておくと、特に何もせず API document をブラウザで閲覧できます。

API 定義していく上で最低限守るべきルール

一個ずつ説明していくと長くなるため、ここではざっと API を定義していく上で弊社で守っているルールをいくつか記述します。基本的にはルールがあったとしても、人間は完璧ではないため、これをどうやって抜けがない状態に仕上げて、本番へ適用するかは仕組みである程度はカバーする必要があります。

  • 必須プロパティには required は必ずつける
    • 特に response は見落としがちなので注意
  • tag は必ずつける
    • エンドポイントが多くなるため、カテゴライズして整理しましょう
    • フロントのコードを自動生成した際によくわからない関数が爆誕します
  • description はなるべく書く
    • パット見てわかれば OK です
  • operationId は必ずつける。
    • 命名大事。リーダブルに
    • これがそのままフロントへ自動生成する際の関数名になります
  • additionalProperties: false もつける
    • これが入ってないと、バックエンドの整合性テストで余計なプロパティ入っててもスルーします
    • 気づかずパスワードとか漏れてたら最悪なので、結構重要です
  • $ref を使用して分割して作った後は一枚の file にする
    • Open API Specification に基づいて書いていれば、色々なところでこの yaml or json を読み込めば使い回すことができるのですが、たまに$ref(他ファイル参照)に対応していないツールもあります。そのために、分割して参照しているファイル群を 1 枚の file にまとめたものを自動生成しておくと後々楽できます。

とにかく、端折らないで埋めるところは全て埋めておけば OK です。
一瞬の怠惰で端折って、後で埋めていくのはかなりめんどくさいです。

OpenAPI Generator を用いて、定義したスキーマから TypeScript 自動生成

OpenAPI Generator を使用して、TypeScipt を自動生成します。

元々Swagger codegen でしたが、それから folk されて作られたのがこれになります。
方向性の違いなど色々あって、folk して新たに作っていくに至ったようですね。
https://openapi-generator.tech/docs/fork-qna/

スキーマからコードを自動生成するのであれば、基本的にはこの OpenAPI Generator を使用しておけば問題ないです。

私達は、typescript-axios を使用して axios を wrap した関数と body, params, response の型を自動生成して、フロントで呼び出して使っています。

自動生成したオリジナルのコードは git ignore して、都度スキーマを元に生成して管理する方法を取っています。スキーマを元に出力される結果が自動生成されたコードであるべきなので、それはバージョン管理せず、人の手が加わらない状態で使われることが望ましいからです。

もし、上記で生成された関数に対して、何かひと手間加えたい場合は、それを呼び出した上で関数をラップすればよいため、オリジナルのファイルは git 管理対象外としています。

スキーマとバックエンド API のレスポンスの整合性チェックは必須

定義したスキーマと実装にずれがあったら、元も子もありません。
よって、これらの整合性チェックは必須になります。

私達はバックエンドに Rails を使用しているため、committee-rails を使って実際のスキーマと実装した API が一致していることを request spec にてテストを書いて保証しています。

正常系のテストは上記でカバーできるのですが、異常系(404 など)のテストは個別でケースごとに追加する必要があります。

スキーマ駆動開発をチームでやってみて感じたメリット・デメリット

メリット

フロントエンドの実装は楽に安全になる

各自定義したスキーマを元に、フロントエンドのリクエスト処理を自動生成し、フロント側はそれを呼ぶだけで通信部分が完結します。また、request body, response も型が生成されるため、それを使って型安全な開発ができます。

また、迷ったらスキーマを参照すればよく、内部実装含めバックエンドに聞く必要がありません。入り口の parameter は何を渡して、出口の response では何が返ってくるか、それだけ気にしていれば OK です。変更が必要な場合は API の定義者と適宜コミュニケーションを取ります。

API の設計ファーストで作っていくため、バックエンドの開発者とレビュアーの出戻りが減る

上述しましたが、バックエンドの開発フローとしては下記のとおりです。

  1. まず API を定義
  2. レビュアーが確認
  3. バックエンド実装

API 仕様に関しては、作り終わった後に実装者と確認者で認識のすれ違いがおきず、アウトラインができた段階で確認を投げているため、作っている側、確認する側、共に認識が一致した状態で作りやすくなります。間違った方向性で設計していた場合、それに早めに気づくことができます。そして高速で修正を行えるため、指摘を受けた側もさっと学んで、さっと修正してその上で実装できます。

API Document と実装の乖離がなくなる

API document 置いてきぼり問題とおさらばできます。
API を定義してから、実装にうつるので document との乖離はなくなります。
そもそも、乖離していたら意味がないので、そうならない仕組みをバックエンドでもつ必要があります。

テストに組み込んで、実際の API document と実装内容が異ならないような仕組みは最早必須となります。

デメリット

ツールに不慣れな場合、説明と学習にやや時間を取られる

これは特にこのトピックに限らずですが、発生します。
API を定義していく段階で、弊社は stoplight studio を使用しています。慣れるまでやや時間がかかりますが、すぐ慣れます。しかし yaml や json をゴリゴリ書いていくよりかは遥かに楽で、時間はかかりますが、そこまでではないため、「やや」と表現しています。

最初は丁寧にフローを用意して、それ通りにやるなりしてやってけば慣れます。すぐ慣れます。

予期せぬ形でバックエンドの整合性チェックが通ってしまう場合もある

デメリットというか注意点です。required の指定や nullable の取り扱いなど若干慣れが必要です。
これは別記事で、committee について言及する際に詳しく書こうと思います。

また、フロントエンドのコードの自動生成に関しても、何も考えずにスキーマ定義して、自動生成をするとよくわからない命名で生成されたりするため、気をつけながら運用していく必要があります。

これからもプロダクトを成長させながら、開発手法もアップデートしていきます

今の所良い感じなのですが、これがずっと良い感じであっても面白くないので、チームや外部環境、プロダクトのアップデートに伴って開発手法も色々試していければと思っております。

単純にそっちのほうが面白く、皆が安全に楽しく開発できればそれでいいので、色々やってみて良いものは良い、悪いものは悪い、もっと良くするためにはこうすれば良いと全員で日々進化していければハッピーです ☺️

エンジニア採用強化中

株式会社 overflow ではエンジニアメンバーを大募集中です。Offers の開発や overflow に興味を持たれた方ぜひご応募ください!中の雰囲気を知っていただくため、まずは副業からのスタートも歓迎しています。

https://offers.jp/jobs/2760
https://offers.jp/jobs/2711

とりあえず話を聞いてみたい!という方は当社 CTO とのカジュアル面談をお勧めしています。

https://meety.net/matches/PZIvsSzZrPXM

※上記の求人は本稿執筆時点の情報であり、閲覧時点で変更があった場合はご容赦ください

Discussion

Stoplight を使って API を定義していくのが楽

うちもOpenAPI書いて開発しているんですが、わりとベタにYAML書いてました。これは新規に習得するメンバーも使いやすそうでいいですね。

$ref を使用して分割して作った後は一枚の file にする

こちらって何かツールとか使っているんでしょうか?

コメントありがとうございます!

うちもOpenAPI書いて開発しているんですが、わりとベタにYAML書いてました。これは新規に習得するメンバーも使いやすそうでいいですね。

最初こそ多少慣れが必要ですが、慣れてしまえばポチポチ定義していくだけなので、習得も楽で使いやすいですね!

こちらって何かツールとか使っているんでしょうか?

https://github.com/WindomZ/swagger-merger
を使ってます!
そして、package.jsonに追加しておいて必要なタイミングで呼び出して1枚のYAMLを生成してます
"scripts": {
    "merge_xxxxx": "swagger-merger -i <base api yaml file path> -o <export api yaml file path>",
  },
ログインするとコメントできます