48
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RESTful APIAdvent Calendar 2019

Day 1

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

Last updated at Posted at 2019-12-01

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

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

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

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

アーキテクチャ

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

いつもの というお買い物アプリです。

 2019-12-01 13.34.00.png

ドキュメント何で書いてますか?

  • Excel => つらい
  • Markdown => つらい
  • 何らかのDSLを用いて生成するツール => :innocent:

素でマークダウンを書くのはつらみが深いので何かしらツールを使いましょう。
apiary, api blueprint, APIDOC, etc. 
いろいろありますが、僕が激しくおすすめするのは OpenAPI です。

理由としては、

  • 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
    • スタブサーバーとクライアントSDKの生成
  • Swagger Editor
    • 定義ファイルの編集が行えるリッチエディタ
    • シンタックスのチェックや補完、ホットリロードでのプレビューをサポート
  • Swagger UI
    • HTMLとしてドキュメントをビジュアライズする
    • 定義されたホストに対してリクエストを送信するAPIクライアントとしても使用できる(Postman的な)

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

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

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

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

実際に使ってみよう

今回実現したいことはこちらです。

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

これ全てOpenAPIでできます。

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

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

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

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

config/routes.rb
Rails.application.routes.draw do
  resources :users
end
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 がスタンダードです。

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というツールを使います。
SwaggerUIで調べると Node.js を使ってサーバーを立ち上げる例ばかりが出てきますが、
別で立てるのは面倒なので docker-compose で一緒に立ち上げてしまいます。
イメージが公開されているのでこれをベースにします。

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でドキュメントが表示されます。

 2019-12-01 15.09.43.png

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

2019-12-01_15_09_43.png

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

 2019-12-01 15.09.53.png

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

 2019-12-01 15.10.01.png

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

ドキュメントを楽に記述したい

エディタごとにプラグインがそれぞれあるかと思います。
僕は普段 VS Code を使っているのでこれを入れています。

OpenAPI(Swagger)Editor

サイドバーから特定の場所にジャンプできる機能が重宝します。
OpenAPI Explorer.png

あとはプレビュー用として Swagger Viewer を入れておいてもいいかと思います。

 2019-12-01 15.38.55.png

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

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

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"
 2019-12-01 15.44.28.png

ドキュメントをローカルサーバーのHTTPクライアントとして使いたい

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

SwaggerUI には Try it out というボタンがついています。
2019-12-01_15_09_53.png

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

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

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

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

 2019-12-01 15.57.28.png

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

 2019-12-01 16.01.41.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 という gem を使います。
ドキュメントサーバーを使うのは開発環境だけなので、 development.rb に記述します。

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コマンドと、レスポンスが表示されました。
 2019-12-01 16.58.41.png

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

レスポンスがドキュメントに沿っているか自動テストしたい

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

rspecに組み込んでみます。

Gemfile
gem 'rspec-rails'
docker-compose run web bundle
docker-compose run web rails generate rspec:install
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 という gem を使います。
さらにそれを Rails 用にラップした committee-rails も使います。

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 に書き換えます。

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

テストをこう書きます。

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 を見ながら覚えていくのがオススメです。

[追記]
今回使ったサンプルコード

48
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?