Swaggerとは
SwaggerはREST APIのドキュメントや、テストできるUIを提供してくれるツールです。
APIのドキュメントはこれまでスプレッドシートなどで書かれることが多かったかと思いますが、Swaggerであれば、リッチなWebサイトのようなUIを提供してくれるので、見やすくかつ理解しやすいドキュメントを作ることができます。さらにパラメータを付与するなどして、返り値を確認するテストを実行することができます。
(GETメソッドのパラメータにbodyが用いられていますが、ミスです。ご容赦ください。)
Swaggerによるドキュメント作成の流れ
SwaggerはYAMLまたはJSONで記述され、そのフォーマットはSwagger Specificationといいます。このSpecificationを読み込ませると、先述のUIが出来上がります。Specificationを作成する基本的な方法が2つあります。Swagger Editorという公式に提供されたWeb上のエディタで編集し出力する方法と、ローカルでファイルを作成、編集する方法です。
Swagger Editorはウィンドウの左半分にSpecification、右には左で書かれた内容をリアルタイムに反映するUIで構成されています。ブラウザ内での記述が即座にUIに反映されて確認できるため、慣れていない場合にはオススメです。編集結果に満足したらSpecificationファイルを出力します。
ローカルで作成する場合は、YAMLまたはJSONファイルを作成し、フォーマットに則って記述します。
作成したSpecificationをSwagger Editorに読み込むことで、Swagger UIを確認することができます(APIのドキュメンテーションとして扱える)が、Dockerを用いて自分が用意する環境にホストすることもできます。Dockerではswagger-uiなどのimageが提供されています。方法についてはこちらで紹介しています。
Specificationの記述方法
ここからはSpecificationの書き方を、各構成要素を紹介する形で簡単に解説したいと思います。詳しい書き方は公式やQiita記事を参照ください。
Specificationは大きく下記の構成で成り立ちます。
- swagger
バージョンを指定 - host
APIのホスト先を記載 - schems
APIのスキーム(HTTP/HTTPS)記載 - basePath
APIのベースURLを記載(http://localhost:3000/xxxxx/xxx/api_pathの、/x~の部分) - produces
APIが生成するデータの形式を記載 - info
問い合わせ先などを記載(任意) - paths
APIのパス、引数、レスポンスなど各APIの詳細を記載 - definitions
APIのレスポンスなどに使用するデータ形式(型)を記載 - responses
ステータス返却など汎用的なレスポンスを記載
definitions、responsesは必須ではありませんが、汎用的な部品をまとめて記述量を減らすためにも利用すべきかと思います。Specificationの記述量はあっという間に膨らみますので、削れるところは削りましょう。
definitionsにexampleを記入することで、responseなどに内容の例示をすることができます。
Specificationの分割
Specificationの記述量はすぐに膨らみ、1つのファイルに書き連ねていくと見辛く編集しにくいファイルになってしまいます。
そこで、refという参照先を示すことができるプロパティを使用してファイルを分割していきます。分割したファイルは何らかの形で結合させる必要がありますが、今回はNode.jsによる例を紹介します。
ディレクトリ構造は下記のようにします。
index.yml
update_api_document
swagger.yml
resolve.js
definitions
index.yml
info
index.yml
paths
index.yml
<他各パスを定義した.yml>
responses
index.yml
package.json
yarn.lock
node_modules
ルートディレクトリのindex.ymlは下記の通りです。swagger.ymlの骨格となる情報が記述されています。詳細部分は$refによって外部ファイルに委譲しています。
swagger: "2.0" host: localhost:3001 schemes: - http basePath: /noauth/v1 produces: - application/json info: $ref: ./info/index.yml paths: $ref: ./paths/index.yml definitions: $ref: ./definitions/index.yml responses: $ref: ./responses/index.yml
委譲先の各ディレクトリには必ずindex.ymlが配置され、先に読み込まれるようにしています。pathsのようにファイルを分けられるものは、各ディレクトリの中でも$refを用いて詳細を委譲します。
paths/index.yml
/orders: $ref: ./orders.yml /reservations: $ref: ./reservations.yml
paths/orders.yml
get:
tags:
- "注文管理"
consumes:
- "application/json"
description: "注文一覧を取得"
parameters:
- description: "検索条件"
in: "body"
name: "body"
required: true
schema:
$ref: "#/definitions/OrderFilter"
produces:
- "application/json"
responses:
200:
description: "ok"
schema:
type: "array"
items:
$ref: "#/definitions/OrderGet"
404:
$ref: "#/responses/NotFound"
summary: "注文一覧取得"
注意してほしいのは、参照を続けた先の末端のファイル(上記例ではpaths/orders.yml)の$refは、"#/~"となっているように同じファイル内を指していることです。これは結合後の状態を前提としているためです。
下記infoはただの一例ですが、補足情報を記載します。
info/index.yml
contact:
email: "support@swagger.io"
name: "API Support"
url: "http://www.swagger.io/support"
description: "This is a sample server celler server."
license:
name: "Apache 2.0"
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
termsOfService: "http://swagger.io/terms/"
title: "Swagger Example API"
version: "1.0"
tags:
- name: "注文管理"
description: "注文に関するAPI"
responseには、404などの汎用的なステータス返却などの情報を記述します。
responses/index.yml
BadRequest:
description: 不正なリクエスト
schema:
$ref: "#/definitions/Error"
Unauthorized:
description: ログインしてください
Forbidden:
description: 権限がありません
NotFound:
description: 対象が存在しません
InternalServerError:
description: サーバエラー
続いてNode.jsファイルの内容を紹介します。
watch.jsファイルで、ルートディレクトリ以下に更新があったらプログラムを作動させswagger.ymlを更新するようにします。
watch.js
var chokidar = require('chokidar'); // ファイル監視モジュール
var execSync = require('child_process').execSync; // コマンド実行用モジュール
var watcher = chokidar.watch('.', {
persistent: true, // 監視を続けている間プロセスを終了しない
ignored: 'swagger.yml', // 監視対象外
ignoreInitial: true, // ファイルやフォルダの監視開始時にaddイベントやaddDirイベントを発生させない
cwd: '.' // 基準パス
});
watcher.unwatch('yarn.lock') // 監視対象外
.unwatch('package.json')
.unwatch('node_modules');
var update_swagger_yml = path=> { // コマンドを実行する関数を変数化
console.log(path + ' changed. Update swagger.yml and copy it to viewer.');
execSync('./update_api_document.sh'); // 引数に指定されたファイルのコマンドを実行
}
console.log('start watching...');
watcher.on('add', update_swagger_yml) // ファイルが新規作成された場合、第2引数を実行
.on('addDir', update_swagger_yml) // フォルダが新規作成された場合、第2引数を実行
.on('unlink', update_swagger_yml) // ファイルが削除された場合、第2引数を実行
.on('unlinkDir', update_swagger_yml) // フォルダが削除された場合、第2引数を実行
.on('change', update_swagger_yml); // ファイル内容が変更された場合、第2引数を実行
監視中のコマンドによってswagger.ymlが更新されるので、swagger.ymlは監視対象外にします。
上記watch.jsで指定されている「コマンド内容を記載したファイル」は、下記のようにします。
update_api_document.sh
#!/bin/sh node ./resolve.js > swagger.yml # resolve.jsの実行結果をswagger.ymlに出力 cp ./swagger.yml ../api/src/docs/yaml/ # swagger.ymlを右側(任意の場所)にコピー
resolve.jsは、$refを解決するプログラムです。
resolve.js
var resolve = require('json-refs').resolveRefs;
var YAML = require('js-yaml');
var fs = require('fs');
var root = YAML.load(fs.readFileSync('./index.yml').toString()); // ./index.ymlをロード
var options = {
filter: ['relative', 'remote'], // relativeとremoteのrefを対象とする
loaderOptions: {
processContent: (res, callback) => { // responseの内容を利用するcallbackを定義
callback(null, YAML.load(res.text));
}
}
};
resolve(root, options).then( results => {
console.log(YAML.dump(results.resolved)); // 解決した結果全てをyamlファイルとして出力
});
filterのrelativeとremoteは、相対パスと同じサーバーにあるパスが指定された$refを解決の対象にすることを意味します。他にlocal('#/~')やinvalid(無効なパス)があります。
watch.jsにより変更が監視され、変更があれば、update_api_document.shによってresolve.jsが実行されて、swagger.ymlが生成されます。.shの最後の行で、生成されたファイルの内容を別ディレクトリにコピーしていますが、これはdocker-composeで指定したswagger-uiの読み込み先です。docker-composeによるSwagger UIの使い方はこちらを参照ください。
手間はかかりますが、分割してみると、その見通しの良さに快感を覚えるはずです。
エラーになる時
Swagger UIで確認した時、definitionsが解決できないなどのエラーになる場合、resolveに失敗している可能性があります。
refのresolveプログラムはファイルの上から解決していくため、途中で失敗すると、以降のrefも解決されません。参照に指定されたものがdefinitionsに定義されていないなどがないか確認してみてください。
また、exampleなど文字列の中にVS Codeなどのエディターに表示されない不正な文字が含まれている場合もあります。ルール通りに見えるのに、エラーが起きてしまう場合には、一度definitionsなどをコピーしてSwagger Editorに貼り付けてみてください。Swagger Editorなら不正な文字を検出してくれますし、デバッグに便利です。