ぼくのかんがえたさいきょうのAPIドキュメント運用🤹‍♀️

# ドキュメントちゃんと保守できてますか？

API開発とドキュメントの保守は切っても切れない問題です。

仕様の記述はもちろんのこと、サンプルを試せるAPIクライアントや、仕様に則った実装になっているかテストも自動化したいですよね。

本記事では、現在開発中のAPIアプリケーションで、実際に僕が試行錯誤していく中でたどり着いたベストプラクティスを紹介しようと思います。

# アーキテクチャ
- iOSアプリのバックエンドとしてJSONを返すAPIサーバー
- Rails6 × MySQL5.7 on Docker

[いつもの](https://apps.apple.com/jp/app/%E3%81%84%E3%81%A4%E3%82%82%E3%81%AE-3%E7%A7%92%E3%81%A7-%E4%B8%80%E7%95%AA%E3%81%8A%E5%BE%97%E3%81%AA%E3%81%8A%E8%B2%B7%E3%81%84%E7%89%A9/id1436665195) というお買い物アプリです。

<img width="502" alt=" 2019-12-01 13.34.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/5820f293-5967-c394-6510-7396cbc8677c.png">

# ドキュメント何で書いてますか？
- Excel => つらい
- Markdown => つらい
- 何らかのDSLを用いて生成するツール => :innocent:


素でマークダウンを書くのはつらみが深いので何かしらツールを使いましょう。
[apiary](https://apiary.io/), [api blueprint](https://apiblueprint.org/), [APIDOC](https://apidocjs.com/), etc.　
いろいろありますが、僕が激しくおすすめするのは [OpenAPI](https://www.openapis.org/) です。

理由としては、

- Linux Foundation、Google、IBM、Microsoftなどが協力して仕様の策定に関わっていること
- 歴史が長く周辺ツールが豊富で、拡張性があること
- 表現力豊かな記述方法

が挙げられます。

# Swagger? OpenAPI?

もともと Swagger という名前だったものが、 OpenAPIと名前を変えてバージョン3.0がリリースされました。
Swaggerと聞けば馴染みのある方も多いと思います。
基本的にSwaggerとOpenAPIを読み替えても問題はないのですが、（ドメインとか残ってるし => https://swagger.io/specification/）
Swaggerは2.xまでで、OpenAPIは3.0からになるので、Swaggerのバージョン3というものは厳密には存在しません。

# OpenAPIが優れている理由
使ったことがない人向けに説明をしておくと、
OpenAPI とは、RESTful API を記述するためのフォーマットの標準であり、
その標準を用いて様々なことを解決するためのヘルパーツール群を提供するものです。

ツールは大きく3種類に分けられます。

- [Swagger Codegen](https://swagger.io/tools/swagger-codegen/)
  - スタブサーバーとクライアントSDKの生成
- [Swagger Editor](https://swagger.io/tools/swagger-editor/)
  - 定義ファイルの編集が行えるリッチエディタ
  - シンタックスのチェックや補完、ホットリロードでのプレビューをサポート
- [Swagger UI](https://swagger.io/tools/swagger-ui/)
  - HTMLとしてドキュメントをビジュアライズする
  - 定義されたホストに対してリクエストを送信するAPIクライアントとしても使用できる（Postman的な）

この3つのオープンソースツールを拡張する形で、各言語ごとのラッパーやフレームワークへの組み込みをサポートするライブラリが数多く作成されています。（ex. [swagger-blocks](https://github.com/fotinakis/swagger-blocks)）

自分が実現したいことに合わせて柔軟にモジュールを組み込むことができるので、どのプロジェクトにも導入がしやすいです。

また、先に述べたように、OpenAPI とはただ単にフォーマットを標準化した`仕様`なので、
ツールを通さなくてもその仕様に則って記述した yaml ファイルをただ共有するだけみたいな使い方もできます。
yamlさえあれば、受け取った人は何かのツールを使って自由に拡張利用できるので、非常にポータビリティが高いと言えます。Dockerファイルで環境のやりとりするみたいなイメージに近い :thinking: 

そして錚々たる大企業たちがスポンサーとなっているため、業界の標準となっていくことは確実です。
OpenAPIの記述に慣れておくことは、エンジニアとして必要なスキルになってくるかと思っています。

# 実際に使ってみよう
今回実現したいことはこちらです。

- ドキュメントをブラウザで手軽に確認したい
- ドキュメントを楽に記述したい
- ドキュメントをローカルサーバーのHTTPクライアントとして使いたい
- レスポンスがドキュメントに則っているか自動テストしたい

これ全てOpenAPIでできます。

ただ実際にやろうとするとツールが豊富で選択肢が多い割に、3.0に対応していないものが多かったり、情報がまとまっていなかったり、ベストプラクティスにたどり着くまでに苦労したので、この記事が何かの助けになれば幸いです。

ちなみにですが、2.xと3.xでは破壊的な変更があるので、2系のツールで3系を動かすのは無理があります。
多くのツールで3に対応するissueが上がっているのですが、長い間放置されているものが多いため、3を使おうとすると選択肢は結構狭まります。

## ドキュメントをブラウザで手軽に確認したい

まずはサンプルとなるエンドポイントを実装します。

```rb:config/routes.rb
Rails.application.routes.draw do
  resources :users
end
```

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    user = {
      :name => "sakuraya",
      :age => 26
    }
    render :json => user
  end
end
```

```
$ curl localhost:3000/users
{"name":"sakuraya","age":26}
```

これを OpenAPI のドキュメントとして記述します。
yaml と json がサポートされていますが、特に理由がなければ yaml を使うことをお勧めします。

ファイル名は何でもいいんですが、 `openapi.yml` がスタンダードです。

```yaml:doc/openapi.yml
openapi: 3.0.2
info:
  title: "ぼくのかんがえたさいきょうのAPIドキュメント運用"
  description: "サンプルアプリ"
  version: "1.0.0"
tags:
  - name: "users"
    description: "ユーザーAPI"
paths:
  /users:
    get:
      summary: "ユーザーを取得"
      description: "ユーザーを取得"
      tags:
        - "users"
      responses:
        200:
          description: "成功時"
          content:
            application/json:
              schema:
                type: "object"
                properties:
                  name:
                    description: "名前"
                    type: "string"
                    example: "sakuraya"
                  age:
                    description: "年齢"
                    type: "integer"
                    example: 26
                required:
                  - "name"
                  - "age"
```

最低限これだけ書けばOKです。

これをブラウザで見るためには、[SwaggerUI](https://swagger.io/tools/swagger-ui/)というツールを使います。
SwaggerUIで調べると Node.js を使ってサーバーを立ち上げる例ばかりが出てきますが、
別で立てるのは面倒なので docker-compose で一緒に立ち上げてしまいます。
[イメージ](https://hub.docker.com/r/swaggerapi/swagger-ui/)が公開されているのでこれをベースにします。


```yaml:docker-compose.yml
version: '3'

services:
  web: &app_base
    build:
      context: .
    ports:
      - 3000:3000
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    volumes:
      - .:/myapp
      - bundle:/usr/local/bundle
    depends_on:
      - db
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
      TZ: Asia/Tokyo
    command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
    volumes:
      - ./vendor/docker/db/data:/var/lib/mysql
      - ./vendor/docker/db/conf.d:/etc/mysql/conf.d
      - ./vendor/docker/db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    ports:
      - 3306:3306
  doc:
    image: swaggerapi/swagger-ui
    volumes:
      - ./doc/openapi.yml:/usr/share/nginx/html/openapi.yml
    environment:
      API_URL: openapi.yml
    ports:
      - 8080:8080
volumes:
  bundle:
```

`doc` の部分が SwaggerUI です。
`doc` 配下に置いたドキュメントファイルをマウントして、ファイル名を環境変数で指定することで、ドキュメントサーバーが立ち上がるようになっています。
これで http://localhost:8080 にアクセスすると、インタラクティブなUIでドキュメントが表示されます。

<img width="1434" alt=" 2019-12-01 15.09.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/5cef70b5-f27d-711c-901e-3dabc45b09d4.png">

▼それぞれ対応するセクションがこうなっている。

<img width="1434" alt="2019-12-01_15_09_43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/4667bbd8-ef9a-391a-c55a-65de50dc8d7d.png">

▼パスをクリックするとアコーディオンが開いて仕様が表示される。

<img width="1428" alt=" 2019-12-01 15.09.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/5aeabb7e-0f6b-8628-c953-610ce2d4b711.png">

▼上の画像は `Example Value` を表示したもので、 `Schema` をクリックすると、プロパティの説明、型、requiredの有無、nullableの有無など詳細が表示される。

<img width="1412" alt=" 2019-12-01 15.10.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/5bf3b783-467f-00f8-6971-95ad9b32cd5f.png">

ドキュメントサーバーとAPIサーバーの立ち上げをいっぺんに管理できるのが便利 :innocent:

## ドキュメントを楽に記述したい
エディタごとにプラグインがそれぞれあるかと思います。
僕は普段 VS Code を使っているのでこれを入れています。

[OpenAPI（Swagger）Editor](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi)

サイドバーから特定の場所にジャンプできる機能が重宝します。
![OpenAPI Explorer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/ddec1c55-d33d-f13f-254b-981d94432ac5.png)

あとはプレビュー用として [Swagger Viewer](https://marketplace.visualstudio.com/items?itemName=Arjun.swagger-viewer) を入れておいてもいいかと思います。

<img width="1436" alt=" 2019-12-01 15.38.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/e621faeb-8341-e59c-859d-e3cf9639e89e.png">

ホットリロードで確認しながらできるので便利です。
一つだけ注意点があって、たまに表示がおかしくなったり、正しく表示されないことがあります（`$ref` が展開されないなど）。
なので僕は書き方に慣れるまではこれを使っていましたが、いまはブラウザでまとめてチェックしています。


あとは書き方のTipsですが、 `components` を使って共通化していくと見通しが良くなります。

```yaml:doc/openapi.uml
paths:
  /users:
    get:
      summary: "ユーザーを取得"
      description: "ユーザーを取得"
      tags:
        - "users"
      responses:
        200:
          description: "成功時"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
components:
  schemas:
    User:
      description: "ユーザー"
      type: "object"
      properties:
        name:
          description: "名前"
          type: "string"
          example: "sakuraya"
        age:
          description: "年齢"
          type: "integer"
          example: 26
      required:
        - "name"
        - "age"
```

<img width="1428" alt=" 2019-12-01 15.44.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/90dc4111-a2b2-e664-6882-ba4c6bc71818.png">


## ドキュメントをローカルサーバーのHTTPクライアントとして使いたい
せっかく docker-compose してるんだから、ローカルのサーバーに実際につないで動かしたいですよね？
API開発時のHTTPクライアントにはずっと [Postman](https://www.getpostman.com/) や、
最近だと [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) なんかを使っていましたが、ドキュメントの変更に対する反映が面倒だったりするのが難点です。
OpenAPIを組み込めればそんな手間もなくなります。

SwaggerUI には `Try it out` というボタンがついています。
<img width="1428" alt="2019-12-01_15_09_53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/75f6df5d-c7a4-b42b-4e97-4f2669431a65.png">

このままでは利用できないので設定を行います。
`servers` というセクションを追記してください。

```yaml:doc/openapi.yml
servers:
  - url: "http://localhost:3000"
    description: "local api server"
```

これがボタンを押した時のリクエスト先のベースURLとなります。

▼トップに `servers` の設定が反映されました。複数設定することができ、切り替えられるようになっています。


<img width="1080" alt=" 2019-12-01 15.57.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/9ff38113-ac20-7af8-629e-89ad50d51c2a.png">

▼ボタンを押すとパラメータが編集できるようになり、 `Execute` ボタンが現れます。
（今回はパラメータないが適当に書くとこんな感じ。よしなにクエリパラメータに突っ込んでくれる。）

<img width="1400" alt=" 2019-12-01 16.01.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/ee5e7188-5aa9-8eb1-5eee-fc849f44c5dc.png">


もう一つ設定が必要で、このままリクエストを送ってもこのようなエラーが出ます。

>Access to fetch at 'http://localhost:3000/users?some_condition=true' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.Access to fetch at 'http://localhost:3000/users?some_condition=true' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Cross Origin の制約を回避するために、Rails側に設定が必要です。
[rack-cors](https://github.com/cyu/rack-cors) という gem を使います。
ドキュメントサーバーを使うのは開発環境だけなので、 `development.rb` に記述します。

```ruby:config/environments/development.rb
config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "localhost:8080"
    resource "*", :headers => :any, :methods => :any
  end
end
```

これでサーバーを再起動すれば、リクエストできるようになります。

▼生成されたcurlコマンドと、レスポンスが表示されました。
<img width="1414" alt=" 2019-12-01 16.58.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/768a9b8b-6562-cdaa-f441-80ef6db9eda8.png">

いちいち Postman 開いてリクエスト書く必要がないので幸せ:innocent:

## レスポンスがドキュメントに沿っているか自動テストしたい
これだけでもだいぶ開発が捗るようになったんですが、テストまでいきたいです。
ドキュメントで Integer になってるプロパティが String で返ってきたり、
required なプロパティがレスポンスになかったら落ちるようにしたいですよね。

rspecに組み込んでみます。

```ruby:Gemfile
gem 'rspec-rails'
```

```sh
docker-compose run web bundle
docker-compose run web rails generate rspec:install
```

```rb:spec/requests/users_spec.rb
require "rails_helper"

RSpec.describe UsersController, :type => :request do
  describe "#index" do
    let(:path) { users_path }  
    it  do
      get path
      expect(JSON.parse(response.body)).to match(
        # いい感じにドキュメントのスキーマを検証したい
      )
    end
  end 
end
```

[committee](https://github.com/interagent/committee) という gem を使います。
さらにそれを Rails 用にラップした [committee-rails](https://github.com/willnet/committee-rails) も使います。

```ruby:Gemfile
gem 'committee'
gem 'committee-rails'
```

設定を追加。

```spec/rails_helper.rb
config.add_setting :committee_options
config.committee_options = { :schema_path => Rails.root.join("doc", "openapi.yml").to_s }
include ::Committee::Rails::Test::Methods
```

テストを落とすために、 Integer であるはずの年齢を String に書き換えます。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    user = {
      :name => "sakuraya",
      :age => "26"
    }
    render :json => user
  end
end
```

テストをこう書きます。

```rb:spec/requests/users_spec.rb
require "rails_helper"

RSpec.describe UsersController, :type => :request do
  describe "#index" do
    let(:path) { users_path }  
    it do
      get path
      assert_request_schema_confirm
      assert_response_schema_confirm
    end
  end 
end
```

実行すると。。。

```
F

Failures:

  1) UsersController#index
     Failure/Error: assert_response_schema_confirm

     Committee::InvalidResponse:
       #/components/schemas/User/properties/age expected integer, but received String: 26
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3/operation_wrapper.rb:35:in `rescue in validate_response_params'
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3/operation_wrapper.rb:30:in `validate_response_params'
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3/response_validator.rb:20:in `call'
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/schema_validator/open_api_3.rb:38:in `response_validate'
     # /usr/local/bundle/gems/committee-3.3.0/lib/committee/test/methods.rb:27:in `assert_response_schema_confirm'
     # ./spec/requests/users_spec.rb:9:in `block (3 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # OpenAPIParser::ValidateError:
     #   #/components/schemas/User/properties/age expected integer, but received String: 26
     #   /usr/local/bundle/gems/openapi_parser-0.6.1/lib/openapi_parser/schema_validator.rb:62:in `validate_data'

Finished in 0.11266 seconds (files took 3.96 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/requests/users_spec.rb:6 # UsersController#index
```

無事落ちました！

▼requiredな値がないときはこうなったりします。

```
 1) UsersController#index
     Failure/Error: assert_response_schema_confirm

     Committee::InvalidResponse:
       #/components/schemas/User missing required parameters: age
```

テストまでかけるとか最高か:innocent:

普段の開発フローとしては、まずドキュメントを記述して仕様をレビュー => テスト書く => 実装というドキュメント&テスト駆動開発でやっています。


# まとめ
今回は既存のプロジェクトに後から組み込んだので使ってませんが、
ドキュメントからモックサーバーを立ち上げたり、コードを生成したりするツールも言語ごとにいろいろあります。
うまく活用してAPIドキュメント運用のつらみから解放されましょう:innocent:

OpenAPI の記述方法については、[OpenAPI-Specification](https://github.com/OAI/OpenAPI-Specification) を見ながら覚えていくのがオススメです。

**[追記]**
今回使った[サンプルコード](https://github.com/Blue-Pix/apple_pie)