以前から使ってみたいと思っていたので使ってみたのと、社内の勉強会で説明する機会があったので内容を残しておきます。

API ドキュメントツール


あまり詳しくはないのですが、OpenAPIとSwaggerというのはAPIドキュメントツールでそれぞれ仕様と実装を指します。そもそもAPIのドキュメントツールが何をできるのかという話ですが、私の認識では下記のような感じです。

  • 仕様を基にドキュメント生成できる
  • モック立てたりできる
  • コードに記載したコメントからドキュメント生成できる

などなど…

OpenAPI Specification


仕様はここにあります

Swagger と OpenAPI の違い


What Is the Difference Between Swagger and OpenAPI?

全て公式の記事に書いてありますが、かいつまんで書くと(たぶん)下記のような感じになります。

  • OpenAPI: 仕様
  • Swagger: 仕様(OpenAPI)をもとにした実装

もともとSwagger SpecificationだったものがOpenAPI Specificationに改名されたものの、実装の方はもとのブランドを維持したまま残した。といことらしいです。OpenAPIに沿った実装はほかにも存在します。

Swagger


以下、今回の説明ではSwaggerを使います。SwaggerはSwagger Editorというオンラインのサービスで手っ取り早く試すことができます。

ただし、実際には手元で確認したいことが多いと思いますので、編集などを行うにはVSCodeの拡張のSwagger Viewerを利用するのが良いでしょう。

動かしながら試す


いきなり大きいサンプルではじめても大変なので、小さいサンプルから動かしながら試してみましょう。Swagerのサンプルがペットショップなので、それにあやかってリカーショップのAPIドキュメントを作成してみましょう。

指定したIDのお酒を返すGETメソッド

とりあえず指定したIDのお酒を返すGETメソッドのみを記述してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
openapi: 3.0.2
info:
title: Liquors Store API
description: サンプルAPI
version: 1.0.0
servers:
- url: /
paths:
/liquors/{liquorId}:
get:
summary: 指定されたidのお酒を取得します
parameters:
- name: liquorId
in: path
description: 取得したいお酒のID
required: true
schema:
type: integer
format: int32
responses:
200:
description: 成功時のレスポンス
content:
'*/*':
schema:
type: object
properties:
id:
type: integer
format: int32
name:
type: string
example: ビール
components: {}

先ほどのVSCode拡張のSwagger Viewrで確認してみましょう。

上記のyamlをVSCodeで開いた状態で、Ctrl + Shift + PSwaggerと入力するとPreview Swaggerというメニューが表示されます。選択すると別のペインでAPIのプレビューが確認できます。

お酒を登録できるようにする

次にPOSTメソッドを追加してみましょう。paths以下に次のように記述します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
paths:
/liquors:
post:
summary: お酒を新規登録します
requestBody:
required: true
content:
application/json:
schema:
properties:
category:
type: string
description: お酒の種別
name:
type: string
description: 銘柄

次に必要なのはレスポンスです。成功時・失敗時の2つのレスポンスを記述します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
paths:
/liquors:
post:
summary: お酒を新規登録します
requestBody:
required: true
content:
application/json:
schema:
properties:
category:
type: string
description: お酒の種別
name:
type: string
description: 銘柄
# こっから下を足す
responses:
201:
description: リクエストに成功し、お酒が作成された場合のレスポンス
content:
application/json:
schema:
type: object
properties:
id:
type: integer
format: int32
category:
type: string
example: ビール
name:
type: string
example: 黒ラベル
400:
description: ペイロードが不正だった場合のレスポンス
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: エラー内容

こんな感じでしょうか。

共通箇所を切り出す

さて、ここまででGETPOSTのレスポンス(schema)が重複していることに気づいたと思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
content:
application/json:
schema:
type: object
properties:
id:
type: integer
format: int32
category:
type: string
example: ビール
name:
type: string
example: 黒ラベル

この重複箇所を切り出します。切り出すにはcomponents.schemasというキーを記述し、そこに共通化したいschemaを記述します。Swaggerのサンプルでは下部に書いてあるので、そちらを参考にしてyamlの下部に書きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
components:
schemas:
liquor:
type: object
properties:
id:
type: integer
format: int32
category:
type: string
example: ビール
name:
type: string
example: 黒ラベル

後はこの切り出したschemasを参照します。例えばPOSTのレスポンスから参照するには下記のようになります。

1
2
3
4
5
6
7
responses:
201:
description: リクエストに成功し、お酒が作成された場合のレスポンス
content:
application/json:
schema:
$ref: '#/components/schemas/liquor'

後はエラーメッセージなども同様に切り出しておいた方が楽になったりするんじゃないでしょうか。

Enumを使う

さて、カテゴリーですがstringよりはenumの方が適切でしょう。

1
2
3
4
5
6
7
8
9
10
11
12
liquor:
type: object
properties:
id:
type: integer
format: int32
category:
type: string
example: ビール
name:
type: string
example: 黒ラベル

typestringのままでenumを追加します。下記のような感じです。

1
2
3
4
5
6
7
8
9
category:
type: string
description: お酒の種別
example: ビール
enum:
- ビール
- 日本酒
- ワイン
- ウイスキー

配列を使う

一件ずつidを指定してGETするわけにもいかないので、まとめて全部取得できるAPIが欲しいですね。

typearrayを指定してitemsキーを記述します。

1
2
3
4
schema:
type: array
items:
$ref: '#/components/schemas/liquor'

これをresponseのキーに指定すると下記のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
/liquors:
get:
summary: お酒の一覧を取得します
responses:
200:
description: 成功時のレスポンス
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/liquor'

認証を扱う

実際のAPIには認証が必要な場合があるかと思います。認証を指定するにはcomponents.securitySchemesに認証方法を記述します。

1
2
3
4
5
6
7
8
9
securitySchemes:
OAuth2:
type: oauth2
description:
flows:
authorizationCode:
scopes:
write: 登録・更新
read: 取得

POST時に認証が必要という想定で、記述したsecuritySchemesをもちいて認証を表現すると下記のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
post:
summary: お酒を新規登録します
requestBody:
required: true
content:
application/json:
schema:
properties:
category:
type: string
description: お酒の種別
enum:
- ビール
- 日本酒
- ワイン
- ウイスキー
name:
type: string
description: 銘柄
security: # これを指定する
- OAuth2:
- write

また、認証失敗の場合のレスポンスも追記するのが良いと思います。

1
2
401:
description: 認証に失敗した場合のレスポンス

ブラッシュアップ

もう少しだけドキュメントをブラッシュアップしたいと思います。

説明の追記

説明をもう少し充実させてみます。yamlは|で改行できます。また、後述するReDoc(OpenAPIのyamlからHTML生成してくれるツール)はイイ感じにやってくれるのでMarkdownの見出しとかも記述できます。

1
2
3
4
5
description: |
# 前書き
このAPI仕様書はOpenAPI形式で記述したものを[ReDoc](https://github.com/Rebilly/ReDoc)でHTML生成しています。
# 概要
本ドキュメントではリカーショップの登録・取得についての仕様を記します。

外部リソースの指定

下記を追記することで外部リソースへのリンクを記述することが可能です。

1
2
3
externalDocs:
description: ソースコード
url: 'https://github.com/YoshinoriN/monooki/tree/master/tools/open-api-v3-with-redoc'

ベースURL

APIのベースURLも細かく指定します。サブディレクトリないしサブドメインでバージョンを分けるのが一般的だと思われるので、下記のような記述をしてみます。

1
2
servers:
- url: http://example.com/v1

出来上がったもの


ここまで手順通りにやれば、下記のようなyamlが出来上がっていると思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
openapi: 3.0.2
info:
title: Liquors Store API
description: |
# 前書き
このAPI仕様書はOpenAPI形式で記述したものを[ReDoc](https://github.com/Rebilly/ReDoc)でHTML生成しています。
# 概要
本ドキュメントではリカーショップの登録・取得についての仕様を記します。
version: 1.0.0
externalDocs:
description: ソースコード
url: 'https://github.com/YoshinoriN/monooki/tree/master/tools/open-api-v3-with-redoc'
servers:
- url: http://example.com/v1
paths:
/liquors:
get:
summary: お酒の一覧を取得します
responses:
200:
description: 成功時のレスポンス
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/liquor'
post:
summary: お酒を新規登録します
requestBody:
required: true
content:
application/json:
schema:
properties:
category:
type: string
description: お酒の種別
enum:
- ビール
- 日本酒
- ワイン
- ウイスキー
name:
type: string
description: 銘柄
security:
- OAuth2:
- write
responses:
201:
description: リクエストに成功し、お酒が作成された場合のレスポンス
content:
application/json:
schema:
$ref: '#/components/schemas/liquor'
400:
description: ペイロードが不正だった場合のレスポンス
content:
application/json:
schema:
$ref: '#/components/schemas/errorMessage'
401:
description: 認証に失敗した場合のレスポンス
/liquors/{liquorId}:
get:
summary: 指定されたidのお酒を取得します
parameters:
- name: liquorId
in: path
description: 取得したいお酒のID
required: true
schema:
type: integer
format: int32
responses:
200:
description: 成功時のレスポンス
content:
application/json:
schema:
$ref: '#/components/schemas/liquor'
components:
schemas:
liquor:
type: object
properties:
id:
type: integer
format: int32
category:
type: string
description: お酒の種別
example: ビール
enum:
- ビール
- 日本酒
- ワイン
- ウイスキー
name:
type: string
description: 銘柄
example: 黒ラベル
errorMessage:
type: object
properties:
message:
type: string
example: エラー内容
securitySchemes:
OAuth2:
type: oauth2
description:
flows:
authorizationCode:
scopes:
write: 登録・更新
read: 取得

Redoc


さて、記述したyamlからドキュメントがきれいな感じで生成されるのが好ましいので、今回はReDocを用いて生成を行います。ReDocを用いてドキュメントを生成すると下記のようなイイ感じのものが出来上がります。

今回はcliを使用するとして、まずpackage.jsonを準備します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "openapi-v3-with-redoc",
"version": "1.0.0",
"description": "Open API Example with Redoc",
"main": "index.js",
"scripts": {
"build": "./node_modules/.bin/redoc-cli bundle ./docs/v1/example.yml -o ./dist/index.html",
},
"author": "YoshinoriN",
"devDependencies": {
"redoc-cli": "^0.8.3"
},
"dependencies": {}
}

後はnpm installしてscriptsにyamlと出力先を指定して実行すればイイ感じにドキュメントを生成してくれます。

おわり。