72
77

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-10-12

今回は、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でも、リクエストヘッダーにキーワードを載せることで期待するレスポンスも得られるようになります
スクリーンショット 2023-10-10 9.54.21.png
スクリーンショット 2023-10-10 9.54.31.png

具体的な書き方はこちら!

~~ 略 ~~
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

自動生成結果はこちら
スクリーンショット 2023-10-11 9.25.34.png

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

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

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

最後に

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

シリーズ記事

72
77
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
77

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?