API BlueprintでAPIドキュメントを作成してGitHub、TravisCI、S3で運用する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

APIドキュメント作成の経緯

クラスメソッドではモバイルアプリケーションのAPIサーバを作ることが多いのですが、フロントエンド担当者とのやりとりでどうしてもドキュメントが必要になります。今までいろんな形で作ってきたのですがどれも問題がありました。

  • フリーフォーマットでWikiに書く。いつの間にか誰も管理しなくなる(できなくなる)。そして誰も見なくなる。
  • コードからドキュメント自動生成の方法のみで運用するとAPIのコードを書ける人しか編集できなくなる
    • 結局待ち時間が発生する
    • フロントの実装者も編集できるようにしたい
  • モックサーバのデータとドキュメントを別々に運用していると、どちらかの更新を忘れる

今回、新しくAPIドキュメントを作るにあたり、今までの問題も踏まえ次のようなフローでドキュメント作成、運用をしてみました。

  1. ドキュメントはマークダウンで記述して、ドキュメントからHTML、開発用のモックサーバを生成する
  2. 生成したHTMLドキュメントはS3のバケットに置く。S3のWebサイトホスティング機能を使って常に最新版のドキュメントをブラウザで見れるようにする
  3. ドキュメントの運用はGitHub、変更はプルリクベースで行う運用にする

API Blueprint

APIドキュメント作成ツールを調べていると、実現したいことがAPI Blueprintでできることが分かりました。
API Blueprint

api-blueprint

API BlueprintはAPIのドキュメント記述ルールを定義していて、そのルールで作成したドキュメントからHTMLの生成やモックサーバの作成をしてくれる様々なツールが用意されています。

記述ルールはマークダウン拡張です。詳細は公式ページやサンプルを参照してください。
API Blueprintのチュートリアル
マークダウンのサンプルが置いてあるリポジトリ

以下はAPI Blueprintのルールで記述したサンプルです。

FORMAT: 1A

# Group Questions

## Question Collection [/questions]

### List All Questions [GET]

+ Response 200 (application/json)

        [

            {
                "question": "Favourite programming language?",
                "published_at": "2014-11-11T08:40:51.620Z",
                "url": "/questions/1",
                "choices": [
                    {
                        "choice": "Swift",
                        "url": "/questions/1/choices/1",
                        "votes": 2048
                    }, {
                        "choice": "Python",
                        "url": "/questions/1/choices/2",
                        "votes": 1024
                    }, {
                        "choice": "Objective-C",
                        "url": "/questions/1/choices/3",
                        "votes": 512
                    }, {
                        "choice": "Ruby",
                        "url": "/questions/1/choices/4",
                        "votes": 256
                    }
                ]
            }
        ]

### Create a New Question [POST]

+ question (string) - The question
+ choices (array[string]) - A collection of choices.

+ Request (application/json)

            {
                "question": "Favourite programming language?",
                "choices": [
                    "Swift",
                    "Python",
                    "Objective-C",
                    "Ruby"
                ]
            }+ Response 201 (application/json)

    + Headers

            Location: /questions/1

    + Body

                {
                    "question": "Favourite programming language?",
                    "published_at": "2014-11-11T08:40:51.620Z",
                    "url": "/questions/1",
                    "choices": [
                        {
                            "choice": "Swift",
                            "url": "/questions/1/choices/1",
                            "votes": 0
                        }, {
                            "choice": "Python",
                            "url": "/questions/1/choices/2",
                            "votes": 0
                        }, {
                            "choice": "Objective-C",
                            "url": "/questions/1/choices/3",
                            "votes": 0
                        }, {
                            "choice": "Ruby",
                            "url": "/questions/1/choices/4",
                            "votes": 0
                        }
                    ]
                }

## Question [/questions/{question_id}]

### Get a Question [GET]

+ Parameters
  + id (string)

+ Response 200 (application/json)

        {
            "id": 1
            "question": "Favourite programming language?",
            "published_at": "2014-11-11T08:40:51.620Z",
            "choices": [
                    {
                        "choice": "Swift",
                        "url": "/questions/1/choices/1",
                        "votes": 2048
                    }, {
                        "choice": "Python",
                        "url": "/questions/1/choices/2",
                        "votes": 1024
                    }, {
                        "choice": "Objective-C",
                        "url": "/questions/1/choices/3",
                        "votes": 512
                    }, {
                        "choice": "Ruby",
                        "url": "/questions/1/choices/4",
                        "votes": 256
                    }
                ]
        }

マークダウン -> HTML変換

aglioというnodeモジュールを使います
aglio

npm install -g aglio

もしくはpackage.jsonを作成してnpm install でも良いです。 変換コマンドは

aglio -i input.md output.html

です。うまくレイアウトされたHTMLが生成されます

api-blueprint-html

モックサーバの作成

その名の通り、api-mockというnodeモジュールを使います。
api-mock

npm install -g api-mock

でインストールして、

api-mock hoge.md

とするとモックサーバがローカルに立ち上がります。
試しにAPIをcurlコマンドで呼び出してみましょう。

curl http://localhost:3001/questions/1
{
    "id": 1
    "question": "Favourite programming language?",
    "published_at": "2014-11-11T08:40:51.620Z",
    "choices": [
            {
                "choice": "Swift",
                "url": "/questions/1/choices/1",
                "votes": 2048
            }, {
                "choice": "Python",
                "url": "/questions/1/choices/2",
                "votes": 1024
            }, {
                "choice": "Objective-C",
                "url": "/questions/1/choices/3",
                "votes": 512
            }, {
                "choice": "Ruby",
                "url": "/questions/1/choices/4",
                "votes": 256
            }
        ]
}

ドキュメントに定義したレスポンスが返却されています。

S3への配信

さて、共通ルールに沿ったドキュメントの作成、HTMLへの変換、モックサーバの作成はできました。 後はS3へ配信するだけですが、せっかくなのでCIサーバを使って自動化しましょう。現在のプロジェクトではCIサービスにTravisを使っていますので、Travisを使った次のような仕組みを作ります

  1. MasterブランチにマージされたタイミングでTravisCIでマークダウンファイルをHTMLに変換する
  2. 出力したHTMLをS3にアップロード
  3. S3のWebサイトホスティング機能を使ってメンバーが最新のドキュメントをWebブラウザで確認できるようにする

Travis-Document-Deploy

それではやってみましょう!

.travis.ymlの設定

aglioというnode_moduleを使ってHTMLに変換するので.travis.ymlを以下のように設定しました。aglioコマンドをインストールするために追加でビルド環境を設定する必要があります。

language: node_js
node_js:
  - '4'
cache:
  directories:
    - node_modules # 時間短縮のため、キャッシュする
addons:
  apt:
    sources:
    - ubuntu-toolchain-r-test
    packages:
    - gcc-4.8
    - g++-4.8
env: CXX="g++-4.8" CC="gcc-4.8"
branches:
  only:
    - master
install:
  - npm install
script:
  - api/bin/converter.sh # HTML変換コマンドが書かれたシェルをここに指定           
deploy:
  provider: s3
  access_key_id:
    secure: アクセスキー
  secret_access_key:
    secure: シークレットアクセスキー

  bucket: バケット名
  skip_cleanup: true
  acl: public_read
  endpoint: バケットのURL
  region: ap-northeast-1
  local_dir: api/output # このディレクトリのファイルがS3にアップロードされる

アクセスキーの暗号化

アクセスキーとシークレットアクセスキーはtravisコマンドで暗号化したものを設定します。次のように指定します。

travis encrypt --add deploy.access_key_id 'アクセスキー'
travis encrypt --add deploy.secret_access_key 'シークレットアクセスキー'

travisコマンドについては以下を参照してください。
travisコマンドのリポジトリ

Travis上でマークダウンファイルをHTMLに変換

特にローカル環境で実行するコマンドと変わりませんが、一応変換するシェルを書いておきます。
複数のマークダウンファイルを一つにまとめてからHTMLに変換しています。

#!/bin/bash

cd `dirname $0`

files=()
files+=('../hoge-api.md')
files+=('../fuga-api.md')

echo 'FORMAT: 1A' > ../api-document.md || exit $?
cat ${files[@]} | sed -e '/^FORMAT: 1A/d' >> ../api-document.md || exit $?
../../node_modules/.bin/aglio -i ../api-document.md -o ../output/api-document.html || exit $?

exit 0

まとめ

ここまでで一連の仕組みづくりができました。APIドキュメントを作成、修正後、マスターブランチにマージすると、生成されたHTMLファイルがS3のバケットに配信されます。
これで常に最新のAPIドキュメントをWebブラウザで確認できるようになりました。

API Blueprintはコードからドキュメントの生成もサポートしているので、次はコードからドキュメントを生成する方法を調査してみたいと思います。