.../articles/

committee×OpenAPI×RailsでスキーマファーストなAPI開発

committeeというgemとOpenAPIのスキーマを使ってRailsでスキーマファーストなAPI開発を試してみました。

弊社でWebアプリケーションを制作する場合、フロントエンドとバックエンドは別々に開発することが多いです。
フロントはほぼNuxt.jsですが、APIは誰が主導するかや求められるスピード感などでプロジェクトごとに変わってきます。
私が主導するプロジェクトである程度スピード感も求められる場合は主にRuby On Railsで開発をしています。(以下Railsで開発するという前提で話を進めます)


前提

課題感

フロントエンドとバックエンドのプロジェクトが別々かどうかはあまり問題ではありませんが、APIの実装を担当する人とフロントエンドでAPIの組み込みを担当する人が異なる場合はAPI仕様の認識合わせが一つ大きな問題となります。

組み込みを担当する人がAPI側のコードを読める人であったとしても、組み込みのたびにコードを読み解くことはしたいと思わないでしょうし、APIの実装を担当する人も新しいエンドポイントの追加やメンバーの参加のたびに説明に時間は割きたくないでしょう。

一般的な解決策としてはAPI仕様をドキュメント化することが考えられますが、ただドキュメント化するという考えだけではなかなかうまくいかないのではないでしょうか。

OpenAPIの活用

弊社ではAPI仕様のドキュメント化に OpenAPI を活用しています。

DSLなどからOpenAPI(またはSwagger)の定義ファイルを出力するライブラリもありますが、対応するバージョンがライブラリに依存してしまうことなどもあり個人的にはYAMLで直接書く方法をとっています。

API仕様を記述したYAMLファイルはリポジトリにPushした時点でCircleCIのArtifactsとしてSwagger UIを展開し、プロジェクトの参加者が閲覧できるようにしています。(どこに展開したとしてもこれを見てもらう意識付けは必要ですが😅)

それでもドキュメントを更新するのを忘れてしまった、ドキュメントを更新したけど実装と違ってしまっていた、などの問題は起こりえます。


スキーマファーストな開発

API仕様のドキュメントを書くタイミングがいつであれ、「ドキュメントを書く」という作業は地味ですし実装との関連性がないとどうしても気が重くなります。

なにかいい方法はないかと探していたところ、OpenAPIのスキーマをもとにAPIレスポンスのテストを書くことができる committee というgemを紹介する記事があり、使ってみたところなかなか良さそうだったので簡単に使い方の紹介とどう活用できるかを考えてみました。

committeeを使ったテストの例

細かいセットアップはここでは割愛しますが、例えば以下のようなAPI仕様を定義したとします。

openapi: 3.0.2

info:
  title: example
  version: '1'

servers:
  - url: http://localhost:3000/{api_version}
    description: local server
    variables:
      api_version:
        default: 'v1'
        enum:
          - 'v1'

components:
  schemas:
    User:
      type: object
      description: user
      required: [id, email, first_name, last_name]
      properties:
        id:
          type: integer
          description: user id
        email:
          type: string
          format: email
          description: email address
        first_name:
          type: string
          description: first name
        last_name:
          type: string
          description: last name

paths:
  /users/{user_id}:
    get:
      tags: [user]
      parameters:
        - in: path
          name: user_id
          description: user id
          required: true
          schema:
            type: integer
      responses:
        200:
          description: ok
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

この例ではバージョニングできるようパスを切っていますが、設定は以下のように prefix を追加することで対応可能です。committee-railsというgemを使うとセットアップが簡単にできます。

  config.include Committee::Rails::Test::Methods
  config.add_setting :committee_options
  config.committee_options = {
    schema_path: Rails.root.join('doc', 'openapi.yml').to_s,
    prefix: '/v1'
  }

GET /users/{user_id} でユーザー情報を取得するAPIという想定なので、これをRSpecとcommitteeで検証するコードは以下のようになります。

RSpec.describe 'user api', type: :request do
  let(:user) { create(:user) }

  describe 'GET /users/:id' do
    it 'success' do
      get user_path(user.id)
      expect(response).to have_http_status(:ok)
      assert_response_schema_confirm
    end
  end
end

このテストでOpenAPIで記述した仕様と実際のレスポンスが合致しているかどうかを検証できるようになりました。(committee-railsのREADMEには assert_schema_conform を使った例が記載されていますが、deprecationの警告が出ていたので assert_response_schema_confirm を使っています)

スキーマを書くことから始める

OpenAPIで記述した仕様をもとにテストを書くことができるようになったわけですが、これを実際の開発フローでどう活用すると良いか考えてみると、

  1. APIの仕様を検討
  2. 仕様が決まったらOpenAPIに則ってドキュメントを書く
  3. ドキュメントをもとにしたテストを書く
  4. API実装
  5. 組み込みへパス

というのがいいのかなと思いました。

進め方としてはTDDと言えると思いますが、ポイントとしては1, 2の段階でバックエンドとフロントエンドでAPIの仕様について認識を合わせた上でベースとなるドキュメントを作ることかと思います。

一番最初のステップでスキーマが共通認識のもとにできていて、APIはそれを満たす形で実装されることが保障されれば、認識の齟齬による手戻りを減らすことができるのではないでしょうか。

というのを一人で考えていたので、チームで活用していけるかはこれから試していきたいと思っています。

→スキーマからクライアントを生成して組み込む方法はこちらで紹介しています。


余談:committeeで気になったところ

プロパティの検証

Userのスキーマ定義で required: [id, email, first_name, last_name] としていますが、requiredとしないプロパティの有無は検証できません。(明らかに違うタイプを返すなどは検証できます)
仕様として違和感があるわけではないですが、これはプロパティの追加時に記述を忘れてしまうことがありそうだなと思いました。

また、仕様には記載していない created_atupdated_at がレスポンスに含まれていてもテストは通ってしまいます。

仕様に記述したプロパティが過不足ないかを簡単に検証できると良いなと思ったので、ドキュメントなどみつつ考えてみようかと思います。

ファイルの参照が解釈されない

OpenAPIやSwaggerのファイルを記述していくとよくあるつらい点は、内容に対してファイルが肥大化しやすいことだと思います。(エンドポイントはそんなにないのに数百行とか😇)

YAMLやJSONを独自ルールで分割して結合するタスクを実行するというのはよく見る方法ですが、OpenAPIのバージョン3ではファイルの参照が可能になっています。

paths:
  /users:
    $ref: '../resources/users.yaml'
  /users/{userId}:
    $ref: '../resources/users-by-id.yaml'

このように分割したファイルの参照がサポートされているので、追加で結合する処理を走らせたりする必要はありません。

しかし、committeeではファイルの参照が正しく解釈されないようでスキーマの検証には使えませんでした。

やりようはあるのかもしれませんが、結局の所一つのファイルに書き出してそれをcommitteeから参照することになりそうです。

→スキーマを分割して管理する方法についてこちらに書きました。

.../articles/

Articles

記事

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

monorepo管理しているNext.jsをAmplifyにデプロイしようとした際にいくつか躓く内容があったのでまとめておきます。

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

コロナ禍の影響により、リモートワークの導入をおこなっている制作会社も多く、実際に弊社でも導入しています。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトのデザインは、「この形式」という決まりはありません。ECサイトで売り上げを上げるなら、しっかりとしたコンセプトと、コンセプトを決定するまでのリサーチが必要です。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

新型コロナウイルス感染拡大の影響で、リモートワークが主流になり、弊社でも週のほとんどは各自宅で作業をしています。

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaを利用するWebデザイナーの助けになれると嬉しいです。使い方は本記事をご覧ください。

ECの構築方法、おすすめのECサービス。

ECの構築方法、おすすめのECサービス。

ファッションや家電、スーパーの買い物でさえもECサイトを利用することが当たり前になりました。加えて新型コロナウイルスの影響もあり、弊社にも「どんなプラットフォームを利用したら良いか」「どれくらいコストがかかるのか」などECに関するさまざまなご相談を頂きます。

FastAPIのスキーマクラスをOpenAPIから生成する方法

FastAPIのスキーマクラスをOpenAPIから生成する方法

PythonでAPIを構築する要件があり、フレームワークに比較的モダンなFastAPIを採用しました。FastAPIはバックエンドの開発を行えば自動でOepnApi定義を生成する機能が備わっていますが、今回はこれを使わず、事前に用意したOepnApi定義からFastAPIで利用するスキーマクラスを生成する方法を紹介します。

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel + MySQLで全文検索を実装する

とあるPythonのソースで sys.path.append としたく無かった話

とあるPythonのソースで sys.path.append としたく無かった話

とあるプロジェクトのとあるソースコードのレビューをしてた時、「ソースコードの参照がうまくいってなかったので修正しました」とレビュー依頼がきました。 ディレクトリ構造 ``` module L __init__.py L main.py L tests L __init__.py L test_main.py ``` ソースコード ``` python tests/test_main.py sys.path.append(os.path.abspath("..")) from main import fuga ``` 今まで案件でPythonに触れる機会も結構ありましたが、なんとなく使ってきた部分も多く、この書き方が良いのか悪いのか判別できなかったので、改めてPythonのモジュールのインポートに関して調べてみたのでブログにしました。普段PHPを書いている事が多くPythonに関して何も分からないので初心者向けの内容になっていると思います。

GiFT1号目新卒デザイナーの2021年振り返り

GiFT1号目新卒デザイナーの2021年振り返り

いつの間に、年末ですね。入社してもう、9ヶ月も立っていたようです。2021年の振り返りを記事にしました。

すべての記事

お問い合わせ