開発効率を爆上げするswagger術

今回は、swaggerを使って開発効率を爆上げしたいあなたに、ちょっと踏み込んだswagger活用術を紹介したいと思います！

# paths sectionを修正しなければいけない機会を最大限少なくする
paths sectionってそもそも指定すべきプロパティが多いので、ちゃんとAPI仕様を作ろうと思うとどうしても見づらくなります
特に、parametersやrequestBody、responsesが同居すると記述のルールが違うので統一感がなく、階層が深くなり見づらいです
そんなpathsはやはり最大限シンプルに保つべきというのが運用していて思うことです

以下にpathsを最大限シンプルに記述した例を提示しますので、是非参考にしてみてはいかがでしょうか！

```yaml
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: 東京都千代田区丸の内１丁目
        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: システムエラーが発生しました。
```

:::note info
swaggerは大きくなる。大きくなるからこそ関心の範囲を狭めよう
:::

# examplesの記述で返却データの網羅性を高める
結構メンテコストは高まるのですが、やはりAPIが返す可能性のあるデータパターンはなるべく仕様書内に表現されていることが望ましいと思いませんか？
[examples](https://swagger.io/docs/specification/adding-examples/)を活用してクライアントサーバーサイド間で認識の齟齬が起きないよう頑張ってみましょう

swaggerにexamples記述をすると以下のキャプチャのように、レスポンス仕様を複数、UI内で表現し使用者に閲覧させることができます
また、後述のprismでも、リクエストヘッダーにキーワードを載せることで期待するレスポンスも得られるようになります
![スクリーンショット 2023-10-10 9.54.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323088/29f2c2c1-2764-ca2f-ca77-aa973933e259.png)
![スクリーンショット 2023-10-10 9.54.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323088/653cfa3b-4b72-1395-ea04-71028e1f16ca.png)

具体的な書き方はこちら！
```yaml
~~ 略 ~~
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: 北海道札幌市北区北６条西４丁目
        birthday: "2000-04-01"
        age: 23
        sex: FEMALE
        memberType: SPECIAL
        email: hanako@example.com
        phoneNumber: 080-2222-3333
```

:::note info
メンテコストは高くなるが、なるべくexamplesは書いて指定しよう
どの程度まで書くかはプロジェクト内で相談しよう
:::

:::note warn
schemaの内容（リクエストやレスポンスの項目）が変更されたら、examplesの内容も修正が必要になることが大多数です。運用時は注意しましょう
:::

# swaggerをインプットにしてprismでモックサーバー起動
swaggerだけでモックサーバーを起動できるのはご存知ですか？
筆者もよく経験があるのですが、以下のようなケースで活用できます

- バックエンド開発が終わっていない機能のフロントエンド開発を先行したい
- テストのデータ作成のためにDBデータを用意するのが面倒すぎる（一操作するとデータが更新されて再利用もできない）

導入についてはすっごく簡単で、prismをインストールして、swaggerのパスを指定して動かすだけ
こちらの記事が参考になります

https://qiita.com/andynuma/items/bf043b5184d3826d0f92

起動すると以下のようになります
```terminal
% 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がリクエストを受け付けると、受け付けた記録もコンソールに出力します
```terminal
curl http://127.0.0.1:4010/users
```
```terminal
[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を思い出してください！

> リクエストヘッダーにキーワードを載せることで期待するレスポンスも得られるようになります

特定のキーワードをリクエストヘッダーにのせるとモックサーバーからのレスポンスを選択できます

https://meta.stoplight.io/docs/prism/beeaad4dc0227-prism-cli#modifying-responses

具体的にできること
- HTTPステータスコードによるレスポンスの出しわけ（responsesに返却を期待するステータスコードごとのレスポンス定義がされてること）
- examplesに記載されているものの出しわけ（前述のexamplesが定義されていること）

それでは具体的な指定方法を解説するために以下のswaggerを使って動かしてみましょう

```yaml
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: 東京都千代田区丸の内１丁目
          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: 北海道札幌市北区北６条西４丁目
        birthday: "2000-04-01"
        age: 23
        sex: FEMALE
        memberType: SPECIAL
        email: hanako@example.com
        phoneNumber: 080-2222-3333
```

#### ステータスコードを指定してレスポンスを取得する（Prefer: code={ステータスコード}）
```terminal
% curl http://127.0.0.1:4010/users/1 -H 'Prefer: code=404' 
{"code":"","message":"指定されたURLは存在しません。"}
```

#### exampleを指定してレスポンスを取得する（Prefer: example={example名}）
```terminal
# なにも指定しない
% 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":"北海道札幌市北区北６条西４丁目","birthday":"2000-04-01","age":23,"sex":"FEMALE","memberType":"SPECIAL","email":"hanako@example.com","phoneNumber":"080-2222-3333"}
```

:::note info
swaggerに記載されたレスポンスを出し分けたい時は、リクエストヘッダーに以下のように指定する
ステータスコードを指定する：Prefer: code=xxx
exampleを指定する：Prefer: example=xxx
:::

# コードの自動生成
筆者はまだ実は活用したことがないので詳しい解説はできないのですが、こんなものがあるという紹介がしたいのでswagger-codegen、試してみました！

https://qiita.com/Imamura-k/items/33f2059205f794267e76

自動生成が可能な言語やフレームワークはlangsで確認できます
```terminal
% 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](#ここで、先ほど記述したexamplesを思い出してください！) を利用します
mock-sample.ymlをclientフォルダに自動生成するコマンド例
```terminal
% swagger-codegen generate -i ./mock-sample.yml --lang javascript -o ./client
```
自動生成結果はこちら
![スクリーンショット 2023-10-11 9.25.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323088/a8fc95af-e1a1-bca6-8036-ac7177fdde48.png)

#### 実際に生成されたコードをピックアップしてみてみる

src/model/User.js
```javascript
/*
 * 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

```javascript
/*
 * 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
     * ユーザー詳細を取得して返却します &lt;br&gt; 
     * @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内の命名を少し気をつけないと何が何だかわからなくなりそうな気もする・・

自動生成物の保守についてはカスタマイズが効かなくなるので、痒いところに手が届かないもどかしさが出てくる気がします（カスタマイズしてしまった途端、変更差分との突き合わせが必要となるので、結果自動生成が使えなくなる）
初期実装で地道に手を動かさなければいけない場面でとりあえず自動生成して部品を頂戴するぐらいの温度感で使うのがいいのかもしれない


# 最後に
この記事に記載のない技や考え方がありましたら是非コメントで教えてください！

## シリーズ記事
https://qiita.com/yuya_sega/items/0b87e8e7d494f6fa3d69

https://qiita.com/yuya_sega/items/508d623b32fb5faa52e3