今回は、swaggerを使って開発効率を爆上げしたいあなたに、ちょっと踏み込んだswagger活用術を紹介したいと思います!
paths sectionを修正しなければいけない機会を最大限少なくする
paths sectionってそもそも指定すべきプロパティが多いので、ちゃんとAPI仕様を作ろうと思うとどうしても見づらくなります
特に、parametersやrequestBody、responsesが同居すると記述のルールが違うので統一感がなく、階層が深くなり見づらいです
そんなpathsはやはり最大限シンプルに保つべきというのが運用していて思うことです
以下にpathsを最大限シンプルに記述した例を提示しますので、是非参考にしてみてはいかがでしょうか!
openapi: 3.0.3
servers:
- url: http://localhost:3000
info:
title: test-api
version: 0.0.1
tags:
- name: user
description: ユーザー情報
paths:
/users:
get:
tags:
- user
summary: ユーザー一覧API
description: |
ユーザーをデフォルトで全件取得して返却します <br>
idの昇順。
parameters:
- $ref: '#/components/parameters/limitQueryParam'
responses:
200:
$ref: '#/components/responses/GetUsersResponse'
400:
$ref: '#/components/responses/400BadRequest'
500:
$ref: '#/components/responses/500InternalServerError'
post:
tags:
- user
summary: ユーザー登録API
requestBody:
$ref: '#/components/requestBodies/PostUserRequestBody'
responses:
200:
$ref: '#/components/responses/GetUserResponse'
400:
$ref: '#/components/responses/400BadRequest'
500:
$ref: '#/components/responses/500InternalServerError'
/users/{userId}:
get:
tags:
- user
summary: ユーザー詳細API
description: |
ユーザー詳細を取得して返却します <br>
parameters:
- $ref: '#/components/parameters/userIdPathParam'
responses:
200:
$ref: '#/components/responses/GetUserResponse'
400:
$ref: '#/components/responses/400BadRequest'
500:
$ref: '#/components/responses/500InternalServerError'
components:
schemas:
User:
type: object
properties:
id:
type: string
example: 248c8027-b752-db4c-76c1-fb22a05e9591
readOnly: true
name:
type: string
example: 田中太郎
address:
type: string
example: 東京都千代田区丸の内1丁目
birthday:
type: string
format: date
example: "1990-01-01"
age:
type: integer
example: 32
sex:
type: string
enum:
- MALE
- FEMALE
example: "MALE"
memberType:
type: string
enum:
- GENERAL
- SPECIAL
- CHILD
- SENIOR
example: "GENERAL"
required:
- id
- name
UserDetail:
allOf:
- $ref: '#/components/schemas/User'
- properties:
email:
type: string
example: sample@example.com
phoneNumber:
type: string
example: "080-1111-2222"
Error:
type: object
properties:
code:
type: string
description: エラーコード
message:
type: string
description: エラーメッセージ
parameters:
userIdPathParam:
name: userId
in: path
description: ユーザーID
required: true
schema:
type: string
example: 248c8027-b752-db4c-76c1-fb22a05e9591
nullable: false
limitQueryParam:
name: limit
in: query
description: レスポンスの要素数
required: false
schema:
type: integer
minimum: 0
default: 0
requestBodies:
PostUserRequestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserDetail'
responses:
GetUsersResponse:
description: success operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
GetUserResponse:
description: success operation
content:
application/json:
schema:
$ref: '#/components/schemas/UserDetail'
400BadRequest:
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: ""
message: zzzzは必須です。
500InternalServerError:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: ""
message: システムエラーが発生しました。
swaggerは大きくなる。大きくなるからこそ関心の範囲を狭めよう
examplesの記述で返却データの網羅性を高める
結構メンテコストは高まるのですが、やはりAPIが返す可能性のあるデータパターンはなるべく仕様書内に表現されていることが望ましいと思いませんか?
examplesを活用してクライアントサーバーサイド間で認識の齟齬が起きないよう頑張ってみましょう
swaggerにexamples記述をすると以下のキャプチャのように、レスポンス仕様を複数、UI内で表現し使用者に閲覧させることができます
また、後述のprismでも、リクエストヘッダーにキーワードを載せることで期待するレスポンスも得られるようになります
具体的な書き方はこちら!
~~ 略 ~~
paths:
/users/{userId}:
get:
tags:
- user
summary: ユーザー詳細API
description: |
ユーザー詳細を取得して返却します <br>
parameters:
- $ref: '#/components/parameters/userIdPathParam'
responses:
200:
# まずはresponsesを参照。(ここでのrefはexamplesの記述にmustかと言われればmustではない)
$ref: '#/components/responses/GetUserResponse'
components:
schemas:
# schemaはこの解説では割愛。
parameters:
# parametersはこの解説では割愛。
responses:
GetUserResponse:
description: success operation
content:
application/json:
schema:
$ref: '#/components/schemas/UserDetail'
# ここから
examples:
NullProperties:
$ref: "#/components/examples/getUserResponseNullableProperties"
Female:
$ref: "#/components/examples/getUserResponseFemale"
# ここまで
# examplesの具体的な書き方は以下のようになります
# 注意)examplesの内容は結局schema項目を意識して書いていくことになるのですが、
# schema項目とexample項目に過不足がないかというチェック機構はないので注意
# 命名はレスポンス名 + パターン名でどうでしょう
examples:
getUserResponseNullableProperties:
value:
id: 248c8027-b752-db4c-76c1-fb22a05e9591
name: 佐藤二郎
address: null
birthday: null
age: null
sex: null
memberType: null
email: jiro@example.com
phoneNumber: null
getUserResponseFemale:
value:
id: ce56b1d5-3dc0-f460-4a40-098a0e1124fa
name: 山田花子
address: 北海道札幌市北区北6条西4丁目
birthday: "2000-04-01"
age: 23
sex: FEMALE
memberType: SPECIAL
email: hanako@example.com
phoneNumber: 080-2222-3333
メンテコストは高くなるが、なるべくexamplesは書いて指定しよう
どの程度まで書くかはプロジェクト内で相談しよう
schemaの内容(リクエストやレスポンスの項目)が変更されたら、examplesの内容も修正が必要になることが大多数です。運用時は注意しましょう
swaggerをインプットにしてprismでモックサーバー起動
swaggerだけでモックサーバーを起動できるのはご存知ですか?
筆者もよく経験があるのですが、以下のようなケースで活用できます
- バックエンド開発が終わっていない機能のフロントエンド開発を先行したい
- テストのデータ作成のためにDBデータを用意するのが面倒すぎる(一操作するとデータが更新されて再利用もできない)
導入についてはすっごく簡単で、prismをインストールして、swaggerのパスを指定して動かすだけ
こちらの記事が参考になります
起動すると以下のようになります
% prism mock ./mock-sample.yml
[11:23:10] › [CLI] … awaiting Starting Prism…
[11:23:10] › [CLI] ℹ info GET http://127.0.0.1:4010/users
[11:23:10] › [CLI] ℹ info POST http://127.0.0.1:4010/users
[11:23:10] › [CLI] ℹ info GET http://127.0.0.1:4010/users/ipsum
[11:23:10] › [CLI] ▶ start Prism is listening on http://127.0.0.1:4010
prismがリクエストを受け付けると、受け付けた記録もコンソールに出力します
curl http://127.0.0.1:4010/users
[11:24:55] › [HTTP SERVER] get /users ℹ info Request received
[11:24:55] › [NEGOTIATOR] ℹ info Request contains an accept header: */*
[11:24:55] › [VALIDATOR] ✔ success The request passed the validation rules. Looking for the best response
[11:24:55] › [NEGOTIATOR] ✔ success Found a compatible content for */*
[11:24:55] › [NEGOTIATOR] ✔ success Responding with the requested status code 200
[11:24:55] › [NEGOTIATOR] ℹ info > Responding with "200"
こうなってしまえば、ローカルの開発環境内であれば自身のPCで起動しているエミュレーターや、nodeで起動しているvueやreactからでもこのモックサーバーにアクセスできますね
ここで、先ほど記述したexamplesを思い出してください!
リクエストヘッダーにキーワードを載せることで期待するレスポンスも得られるようになります
特定のキーワードをリクエストヘッダーにのせるとモックサーバーからのレスポンスを選択できます
具体的にできること
- HTTPステータスコードによるレスポンスの出しわけ(responsesに返却を期待するステータスコードごとのレスポンス定義がされてること)
- examplesに記載されているものの出しわけ(前述のexamplesが定義されていること)
それでは具体的な指定方法を解説するために以下のswaggerを使って動かしてみましょう
openapi: 3.0.3
servers:
- url: http://localhost:3000
info:
title: test-api
version: 0.0.1
tags:
- name: user
description: ユーザー情報
paths:
/users/{userId}:
get:
tags:
- user
summary: ユーザー詳細API
description: |
ユーザー詳細を取得して返却します <br>
parameters:
- $ref: '#/components/parameters/userIdPathParam'
responses:
200:
$ref: '#/components/responses/GetUserResponse'
404:
$ref: '#/components/responses/404NotFoundError'
500:
$ref: '#/components/responses/500InternalServerError'
components:
schemas:
User:
type: object
properties:
id:
type: string
example: 248c8027-b752-db4c-76c1-fb22a05e9591
readOnly: true
name:
type: string
example: 田中太郎
address:
type: string
example: 東京都千代田区丸の内1丁目
nullable: true
birthday:
type: string
format: date
example: "1990-01-01"
nullable: true
age:
type: integer
example: 32
nullable: true
sex:
type: string
enum:
- MALE
- FEMALE
- null
example: "MALE"
nullable: true
memberType:
type: string
enum:
- GENERAL
- SPECIAL
- CHILD
- SENIOR
- null
example: "GENERAL"
nullable: true
required:
- id
- name
UserDetail:
allOf:
- $ref: '#/components/schemas/User'
- properties:
email:
type: string
example: sample@example.com
phoneNumber:
type: string
example: "080-1111-2222"
Error:
type: object
properties:
code:
type: string
description: エラーコード
message:
type: string
description: エラーメッセージ
parameters:
userIdPathParam:
name: userId
in: path
description: ユーザーID
required: true
schema:
type: string
example: 248c8027-b752-db4c-76c1-fb22a05e9591
nullable: false
responses:
GetUserResponse:
description: success operation
content:
application/json:
schema:
$ref: '#/components/schemas/UserDetail'
examples:
NullProperties:
$ref: "#/components/examples/getUserResponseNullableProperties"
Female:
$ref: "#/components/examples/getUserResponseFemale"
404NotFoundError:
description: Not Found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: ""
message: 指定されたURLは存在しません。
500InternalServerError:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: ""
message: システムエラーが発生しました。
examples:
getUserResponseNullableProperties:
value:
id: 248c8027-b752-db4c-76c1-fb22a05e9591
name: 佐藤二郎
address: null
birthday: null
age: null
sex: null
memberType: null
email: jiro@example.com
phoneNumber: null
getUserResponseFemale:
value:
id: ce56b1d5-3dc0-f460-4a40-098a0e1124fa
name: 山田花子
address: 北海道札幌市北区北6条西4丁目
birthday: "2000-04-01"
age: 23
sex: FEMALE
memberType: SPECIAL
email: hanako@example.com
phoneNumber: 080-2222-3333
ステータスコードを指定してレスポンスを取得する(Prefer: code={ステータスコード})
% curl http://127.0.0.1:4010/users/1 -H 'Prefer: code=404'
{"code":"","message":"指定されたURLは存在しません。"}
exampleを指定してレスポンスを取得する(Prefer: example={example名})
# なにも指定しない
% curl http://127.0.0.1:4010/users/1
{"id":"248c8027-b752-db4c-76c1-fb22a05e9591","name":"佐藤二郎","address":null,"birthday":null,"age":null,"sex":null,"memberType":null,"email":"jiro@example.com","phoneNumber":null}
# Femaleを指定する
% curl http://127.0.0.1:4010/users/1 -H 'Prefer: example=Female'
{"id":"ce56b1d5-3dc0-f460-4a40-098a0e1124fa","name":"山田花子","address":"北海道札幌市北区北6条西4丁目","birthday":"2000-04-01","age":23,"sex":"FEMALE","memberType":"SPECIAL","email":"hanako@example.com","phoneNumber":"080-2222-3333"}
swaggerに記載されたレスポンスを出し分けたい時は、リクエストヘッダーに以下のように指定する
ステータスコードを指定する:Prefer: code=xxx
exampleを指定する:Prefer: example=xxx
コードの自動生成
筆者はまだ実は活用したことがないので詳しい解説はできないのですが、こんなものがあるという紹介がしたいのでswagger-codegen、試してみました!
自動生成が可能な言語やフレームワークはlangsで確認できます
% swagger-codegen langs
Available languages: [dart, aspnetcore, csharp, csharp-dotnet2, go, go-server, dynamic-html, html, html2, java, jaxrs-cxf-client, jaxrs-cxf, inflector, jaxrs-cxf-cdi, jaxrs-spec, jaxrs-jersey, jaxrs-di, jaxrs-resteasy-eap, jaxrs-resteasy, java-vertx, micronaut, spring, nodejs-server, openapi, openapi-yaml, kotlin-client, kotlin-server, php, python, python-flask, r, ruby, scala, scala-akka-http-server, swift3, swift4, swift5, typescript-angular, typescript-axios, typescript-fetch, javascript]
javascriptのコードを実際に生成してみる
inputに使用するファイルは prismの解説で使ったswagger を利用します
mock-sample.ymlをclientフォルダに自動生成するコマンド例
% swagger-codegen generate -i ./mock-sample.yml --lang javascript -o ./client
実際に生成されたコードをピックアップしてみてみる
src/model/User.js
/*
* test-api
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
*
* OpenAPI spec version: 0.0.1
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
*
* Swagger Codegen version: 3.0.47
*
* Do not edit the class manually.
*
*/
import {ApiClient} from '../ApiClient';
/**
* The User model module.
* @module model/User
* @version 0.0.1
*/
export class User {
/**
* Constructs a new <code>User</code>.
* @alias module:model/User
* @class
* @param id {String}
* @param name {String}
*/
constructor(id, name) {
this.id = id;
this.name = name;
}
/**
* Constructs a <code>User</code> from a plain JavaScript object, optionally creating a new instance.
* Copies all relevant properties from <code>data</code> to <code>obj</code> if supplied or a new instance if not.
* @param {Object} data The plain JavaScript object bearing properties of interest.
* @param {module:model/User} obj Optional instance to populate.
* @return {module:model/User} The populated <code>User</code> instance.
*/
static constructFromObject(data, obj) {
if (data) {
obj = obj || new User();
if (data.hasOwnProperty('id'))
obj.id = ApiClient.convertToType(data['id'], 'String');
if (data.hasOwnProperty('name'))
obj.name = ApiClient.convertToType(data['name'], 'String');
if (data.hasOwnProperty('address'))
obj.address = ApiClient.convertToType(data['address'], 'String');
if (data.hasOwnProperty('birthday'))
obj.birthday = ApiClient.convertToType(data['birthday'], 'Date');
if (data.hasOwnProperty('age'))
obj.age = ApiClient.convertToType(data['age'], 'Number');
if (data.hasOwnProperty('sex'))
obj.sex = ApiClient.convertToType(data['sex'], 'String');
if (data.hasOwnProperty('memberType'))
obj.memberType = ApiClient.convertToType(data['memberType'], 'String');
}
return obj;
}
}
/**
* @member {String} id
*/
User.prototype.id = undefined;
/**
* @member {String} name
*/
User.prototype.name = undefined;
/**
* @member {String} address
*/
User.prototype.address = undefined;
/**
* @member {Date} birthday
*/
User.prototype.birthday = undefined;
/**
* @member {Number} age
*/
User.prototype.age = undefined;
/**
* Allowed values for the <code>sex</code> property.
* @enum {String}
* @readonly
*/
User.SexEnum = {
/**
* value: "MALE"
* @const
*/
MALE: "MALE",
/**
* value: "FEMALE"
* @const
*/
FEMALE: "FEMALE",
/**
* value: "null"
* @const
*/
_null: "null"
};
/**
* @member {module:model/User.SexEnum} sex
*/
User.prototype.sex = undefined;
/**
* Allowed values for the <code>memberType</code> property.
* @enum {String}
* @readonly
*/
User.MemberTypeEnum = {
/**
* value: "GENERAL"
* @const
*/
GENERAL: "GENERAL",
/**
* value: "SPECIAL"
* @const
*/
SPECIAL: "SPECIAL",
/**
* value: "CHILD"
* @const
*/
CHILD: "CHILD",
/**
* value: "SENIOR"
* @const
*/
SENIOR: "SENIOR",
/**
* value: "null"
* @const
*/
_null: "null"
};
/**
* @member {module:model/User.MemberTypeEnum} memberType
*/
User.prototype.memberType = undefined;
src/api/UserApi.js
/*
* test-api
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
*
* OpenAPI spec version: 0.0.1
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
*
* Swagger Codegen version: 3.0.47
*
* Do not edit the class manually.
*
*/
import {ApiClient} from "../ApiClient";
import {Error} from '../model/Error';
import {UserDetail} from '../model/UserDetail';
/**
* User service.
* @module api/UserApi
* @version 0.0.1
*/
export class UserApi {
/**
* Constructs a new UserApi.
* @alias module:api/UserApi
* @class
* @param {module:ApiClient} [apiClient] Optional API client implementation to use,
* default to {@link module:ApiClient#instanc
e} if unspecified.
*/
constructor(apiClient) {
this.apiClient = apiClient || ApiClient.instance;
}
/**
* Callback function to receive the result of the usersUserIdGet operation.
* @callback moduleapi/UserApi~usersUserIdGetCallback
* @param {String} error Error message, if any.
* @param {module:model/UserDetail{ data The data returned by the service call.
* @param {String} response The complete HTTP response.
*/
/**
* ユーザー詳細API
* ユーザー詳細を取得して返却します <br>
* @param {String} userId ユーザーID
* @param {module:api/UserApi~usersUserIdGetCallback} callback The callback function, accepting three arguments: error, data, response
* data is of type: {@link <&vendorExtensions.x-jsdoc-type>}
*/
usersUserIdGet(userId, callback) {
let postBody = null;
// verify the required parameter 'userId' is set
if (userId === undefined || userId === null) {
throw new Error("Missing the required parameter 'userId' when calling usersUserIdGet");
}
let pathParams = {
'userId': userId
};
let queryParams = {
};
let headerParams = {
};
let formParams = {
};
let authNames = [];
let contentTypes = [];
let accepts = ['application/json'];
let returnType = UserDetail;
return this.apiClient.callApi(
'/users/{userId}', 'GET',
pathParams, queryParams, headerParams, formParams, postBody,
authNames, contentTypes, accepts, returnType, callback
);
}
}
modelはそのまま使えそうな気もする!
apiはswagger内の命名を少し気をつけないと何が何だかわからなくなりそうな気もする・・
自動生成物の保守についてはカスタマイズが効かなくなるので、痒いところに手が届かないもどかしさが出てくる気がします(カスタマイズしてしまった途端、変更差分との突き合わせが必要となるので、結果自動生成が使えなくなる)
初期実装で地道に手を動かさなければいけない場面でとりあえず自動生成して部品を頂戴するぐらいの温度感で使うのがいいのかもしれない
最後に
この記事に記載のない技や考え方がありましたら是非コメントで教えてください!
シリーズ記事