• トップ
  • ブログ一覧
  • rswag gemを利用してRailsでAPIドキュメントを生成する
  • rswag gemを利用してRailsでAPIドキュメントを生成する

    はじめに

    今回は、RailsアプリケーションにSwaggerを用いてAPIドキュメントを導入する方法について解説します。
    私自身はGraphQLで開発する機会が多く、これまでSwaggerを使うことがほとんどありませんでした。
    そのため、今回初めてSwaggerを使ってみた経験を備忘録として残しておこうと思い、本記事を執筆することにしました。

    rswagのインストール

    今回は「rswag」というgemを使用します。
    こちらはrspecで書いたテストファイルがそのままAPIドキュメントのスキーマになるという優れもので、スキーマと実装の乖離をなくすことができます。
    以下をgemに追加します。

    1gem 'rswag-api'
    2gem 'rswag-ui'
    3
    4group :development, :test do
    5  gem 'rspec-rails'
    6  gem 'rswag-specs'
    7end

    初期設定

    簡単のアプリで検証するためscaffoldを使用してユーザーのCRUD操作機能を追加します。

    1rails generate scaffold User name:string email:string --api

    routes.rbに以下追記

    1resources :users

    scaffoldで自動生成されたコントローラーは以下のようなコードになります。

    1class UsersController < ApplicationController
    2  before_action :set_user, only: %i[ show update destroy ]
    3
    4  # GET /users
    5  def index
    6    @users = User.all
    7
    8    render json: @users
    9  end
    10
    11  # GET /users/1
    12  def show
    13    render json: @user
    14  end
    15
    16  # POST /users
    17  def create
    18    @user = User.new(user_params)
    19
    20    if @user.save
    21      render json: @user, status: :created, location: @user
    22    else
    23      render json: @user.errors, status: :unprocessable_entity
    24    end
    25  end
    26
    27  # PATCH/PUT /users/1
    28  def update
    29    if @user.update(user_params)
    30      render json: @user
    31    else
    32      render json: @user.errors, status: :unprocessable_entity
    33    end
    34  end
    35
    36  # DELETE /users/1
    37  def destroy
    38    @user.destroy!
    39  end
    40
    41  private
    42    # Use callbacks to share common setup or constraints between actions.
    43    def set_user
    44      @user = User.find(params.expect(:id))
    45    end
    46
    47    # Only allow a list of trusted parameters through.
    48    def user_params
    49      params.expect(user: [ :name, :email ])
    50    end
    51end

    マイグレーションを実行してDBにusersテーブルを作成します

    1rails db:migrate

    rspec上でドキュメント生成をするため、以下のgenerateコマンドを実行していきます。

    1rails g rspec:install
    2rails g rswag:api:install 
    3rails g rswag:ui:install
    4RAILS_ENV=test rails g rswag:specs:install

    次にUsersControllerのスキーマ定義付specファイルを生成するため以下を実行します。

    1rails generate rspec:swagger UsersController

    実行すると以下のようにスキーマの枠組みが自動生成されます。
    便利ですね。

    1require 'swagger_helper'
    2
    3RSpec.describe 'users', type: :request do
    4
    5  path '/users' do
    6
    7    get('list users') do
    8      response(200, 'successful') do
    9
    10        after do |example|
    11          example.metadata[:response][:content] = {
    12            'application/json' => {
    13              example: JSON.parse(response.body, symbolize_names: true)
    14            }
    15          }
    16        end
    17        run_test!
    18      end
    19    end
    20
    21    post('create user') do
    22      response(200, 'successful') do
    23
    24        after do |example|
    25          example.metadata[:response][:content] = {
    26            'application/json' => {
    27              example: JSON.parse(response.body, symbolize_names: true)
    28            }
    29          }
    30        end
    31        run_test!
    32      end
    33    end
    34  end
    35
    36  path '/users/{id}' do
    37    # You'll want to customize the parameter types...
    38    parameter name: 'id', in: :path, type: :string, description: 'id'
    39
    40    get('show user') do
    41      response(200, 'successful') do
    42        let(:id) { '123' }
    43
    44        after do |example|
    45          example.metadata[:response][:content] = {
    46            'application/json' => {
    47              example: JSON.parse(response.body, symbolize_names: true)
    48            }
    49          }
    50        end
    51        run_test!
    52      end
    53    end
    54
    55    patch('update user') do
    56      response(200, 'successful') do
    57        let(:id) { '123' }
    58
    59        after do |example|
    60          example.metadata[:response][:content] = {
    61            'application/json' => {
    62              example: JSON.parse(response.body, symbolize_names: true)
    63            }
    64          }
    65        end
    66        run_test!
    67      end
    68    end
    69
    70    put('update user') do
    71      response(200, 'successful') do
    72        let(:id) { '123' }
    73
    74        after do |example|
    75          example.metadata[:response][:content] = {
    76            'application/json' => {
    77              example: JSON.parse(response.body, symbolize_names: true)
    78            }
    79          }
    80        end
    81        run_test!
    82      end
    83    end
    84
    85    delete('delete user') do
    86      response(200, 'successful') do
    87        let(:id) { '123' }
    88
    89        after do |example|
    90          example.metadata[:response][:content] = {
    91            'application/json' => {
    92              example: JSON.parse(response.body, symbolize_names: true)
    93            }
    94          }
    95        end
    96        run_test!
    97      end
    98    end
    99  end
    100end

    ドキュメントの生成と確認

    仮のスキーマですが、現状ドキュメントの表示の確認してみます。
    以下を実行してAPIドキュメントを生成します。

    1rake rswag:specs:swaggerize

    以下のURLにアクセスしてみます。
    http://localhost:3000/api-docs

    するとドキュメントが開けました!
    ちゃんとrspecに記載したAPIのドキュメントが反映されています。

    ただ自動生成されたデータでは不足している定義が多いので編集していきます。

    エンドポイントの変更

    デフォルトでは「https://www.example.com」がアクセスURLになっているため、ローカル用に変更します。
    「swagger_helper.rb」を開き以下のようにURLを変更します
    これで指定したエンドポイントでAPIを実行することができます。

    1servers: [
    2  {
    3    url: 'https://{defaultHost}',
    4    variables: {
    5      defaultHost: {
    6        default: 'www.example.com'
    7      }
    8    }
    9  }
    10]
    11
    12↓以下に変更
    13
    14servers: [
    15  {
    16    url: 'http://localhost:3000',
    17  }
    18]

    スキーマの更新

    現状だと、レスポンスとリクエストパラメーターが定義されていないため追記する必要があります。
    その他テストが成功するように実際の挙動に合わせて微修正します。
    以下のようにテストファイルを更新しました。

    1require 'swagger_helper'
    2
    3RSpec.describe 'users', type: :request do
    4  path '/users' do
    5    get('list users') do
    6      tags 'Users'
    7      produces 'application/json'
    8
    9      response(200, 'successful') do
    10        let!(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    11        schema type: :array,
    12          items: {
    13            type: :object,
    14              properties: {
    15                id: { type: :integer },
    16                name: { type: :string },
    17                email: { type: :string },
    18                created_at: { type: :string, format: 'date-time' },
    19                updated_at: { type: :string, format: 'date-time' }
    20              }, required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
    21          }
    22
    23        after do |example|
    24          example.metadata[:response][:content] = {
    25            'application/json' => {
    26              example: JSON.parse(response.body, symbolize_names: true)
    27            }
    28          }
    29        end
    30        run_test!
    31      end
    32    end
    33
    34    post('create user') do
    35      tags 'Users'
    36      consumes 'application/json'
    37
    38      parameter name: :user, in: :body, schema: {
    39        type: :object,
    40        properties: {
    41          user: {
    42            type: :object,
    43            properties: {
    44              name: { type: :string, example: 'テスト太郎' },
    45              email: { type: :string, example: 'test@example.com' }
    46            },
    47            required: [ 'name', 'email' ]
    48          }
    49        },
    50        required: [ 'user' ]
    51      }
    52
    53      response(201, 'successful') do
    54        let(:user) { { user: { name: 'Test User', email: 'test@example.com' } } }
    55
    56        after do |example|
    57          example.metadata[:response][:content] = {
    58            'application/json' => {
    59              example: JSON.parse(response.body, symbolize_names: true)
    60            }
    61          }
    62        end
    63        run_test!
    64      end
    65    end
    66  end
    67
    68  path '/users/{id}' do
    69    parameter name: 'id', in: :path, type: :string, description: 'User ID'
    70
    71    get('show user') do
    72      tags 'Users'
    73      produces 'application/json'
    74
    75      response(200, 'successful') do
    76        let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    77        let(:id) { existing_user.id }
    78        schema type: :object,
    79          properties: {
    80            id: { type: :integer },
    81            name: { type: :string },
    82            email: { type: :string },
    83            created_at: { type: :string, format: 'date-time' },
    84            updated_at: { type: :string, format: 'date-time' }
    85          }, required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
    86
    87        after do |example|
    88          example.metadata[:response][:content] = {
    89            'application/json' => {
    90              example: JSON.parse(response.body, symbolize_names: true)
    91            }
    92          }
    93        end
    94        run_test!
    95      end
    96    end
    97
    98    patch('update user') do
    99      tags 'Users'
    100      consumes 'application/json'
    101      parameter name: :user, in: :body, schema: {
    102        type: :object,
    103        properties: {
    104          user: {
    105            type: :object,
    106            properties: {
    107              name: { type: :string, example: 'テスト太郎' },
    108              email: { type: :string, example: 'test@example.com' }
    109            }
    110          }
    111        },
    112        required: [ 'user' ]
    113      }
    114
    115      response(200, 'successful') do
    116        let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    117        let(:id) { existing_user.id }
    118        let(:user) { { user: { name: 'Updated User', email: 'updated@example.com' } } }
    119        schema type: :object,
    120          properties: {
    121            id: { type: :integer },
    122            name: { type: :string },
    123            email: { type: :string },
    124            created_at: { type: :string, format: 'date-time' },
    125            updated_at: { type: :string, format: 'date-time' }
    126          }, required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
    127
    128        after do |example|
    129          example.metadata[:response][:content] = {
    130            'application/json' => {
    131              example: JSON.parse(response.body, symbolize_names: true)
    132            }
    133          }
    134        end
    135        run_test!
    136      end
    137    end
    138
    139    put('update user') do
    140      tags 'Users'
    141      consumes 'application/json'
    142      parameter name: :user, in: :body, schema: {
    143        type: :object,
    144        properties: {
    145          user: {
    146            type: :object,
    147            properties: {
    148              name: { type: :string, example: 'テスト太郎' },
    149              email: { type: :string, example: 'test@example.com' }
    150            }
    151          }
    152        },
    153        required: [ 'user' ]
    154      }
    155
    156      response(200, 'successful') do
    157        let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    158        let(:id) { existing_user.id }
    159        let(:user) { { user: { name: 'Updated User', email: 'updated@example.com' } } }
    160
    161        after do |example|
    162          example.metadata[:response][:content] = {
    163            'application/json' => {
    164              example: JSON.parse(response.body, symbolize_names: true)
    165            }
    166          }
    167        end
    168        run_test!
    169      end
    170    end
    171
    172    delete('delete user') do
    173      tags 'Users'
    174
    175      response(204, 'no content') do
    176        let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    177        let(:id) { existing_user.id }
    178
    179        run_test!
    180      end
    181    end
    182  end
    183end

    追加したスキーマについて解説します。

    レスポンススキーマ

    レスポンスの定義方法を以下に示します。
    こちらは「オブジェクトの配列である」ことを表しており、プロパティの型や必須設定(required)の定義を行なっています。

    1schema type: :array,
    2  items: {
    3    type: :object,
    4      properties: {
    5        id: { type: :integer },
    6        name: { type: :string },
    7        email: { type: :string },
    8        created_at: { type: :string, format: 'date-time' },
    9        updated_at: { type: :string, format: 'date-time' }
    10      }, required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
    11  }

    必要があればname: { type: :string, nullable: true}のようにするとnull設定も変更できます(デフォルトはnotnullです)

    リクエストパラメーター

    次にリクエストパラメーターの定義について解説します。
    パスパラメーターは最初から定義されていましたが、まとめて解説します。

    リクエストボディ

    リクエストボディはin: :bodyとすることで定義できます。
    exampleにパラメーター例を指定することで、UI上でパラメーター例を表示することができます。

    1parameter name: :user, in: :body, schema: {
    2  type: :object,
    3  properties: {
    4    user: {
    5      type: :object,
    6      properties: {
    7        name: { type: :string, example: 'テスト太郎' },
    8        email: { type: :string, example: 'test@example.com' }
    9      },
    10      required: [ 'name', 'email' ]
    11    }
    12  },
    13  required: [ 'user' ]
    14}
    パス、クエリストリング

    パスはin: :path、クエリストリングはin: :queryとすることで表現できます。

    1parameter name: 'id', in: :path, type: :string, description: 'User ID'
    1parameter name: 'id', in: :query, type: :string, description: 'User ID'

    スキーマのコンポーネント化

    先ほどのスキーマで機能面としては完成しているのですが、同じ構造を毎回再定義しており冗長です。
    同構造のレスポンスやリクエストボディはコンポーネントとして共通化することができます。

    以下のように「components→schemas」の中にコンポーネントを作成することができ、こちらのコンポーネントを複数のAPIで使い回すことができます。

    1components: {
    2  schemas: {
    3    user_request_body: {
    4      type: :object,
    5      properties: {
    6        user: {
    7          type: :object,
    8          properties: {
    9            name: { type: :string, example: 'テスト太郎' },
    10            email: { type: :string, example: 'test@example.com' }
    11          },
    12          required: [ 'name', 'email' ]
    13        }
    14      },
    15      required: [ 'user' ]
    16    },
    17    user_response: {
    18      type: :object,
    19      properties: {
    20        id: { type: :integer },
    21        name: { type: :string },
    22        email: { type: :string },
    23        created_at: { type: :string, format: 'date-time' },
    24        updated_at: { type: :string, format: 'date-time' }
    25      },
    26      required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
    27    }
    28  }
    29}

    こちらを「spec/swagger_helper.rb」に以下のように定義します。

    1config.openapi_specs = {
    2  'v1/swagger.yaml' => {
    3    openapi: '3.0.1',
    4    info: {
    5      title: 'API V1',
    6      version: 'v1'
    7    },
    8    paths: {},
    9    servers: [
    10      {
    11        url: 'http://localhost:3000'
    12      }
    13    ],
    14    components: {
    15      schemas: {
    16        user_request_body: {
    17          type: :object,
    18          properties: {
    19            user: {
    20              type: :object,
    21              properties: {
    22                name: { type: :string, example: 'テスト太郎' },
    23                email: { type: :string, example: 'test@example.com' }
    24              },
    25              required: [ 'name', 'email' ]
    26            }
    27          },
    28          required: [ 'user' ]
    29        },
    30        user_response: {
    31          type: :object,
    32          properties: {
    33            id: { type: :integer },
    34            name: { type: :string },
    35            email: { type: :string },
    36            created_at: { type: :string, format: 'date-time' },
    37            updated_at: { type: :string, format: 'date-time' }
    38          },
    39          required: [ 'id', 'name', 'email', 'created_at', 'updated_at' ]
    40        }
    41      }
    42    }
    43  }
    44}

    作成したコンポーネントは'$ref' => '#/components/schemas/コンポーネント名' とすると参照することができます。
    最終的なuser系APIのスキーマは以下のようになります。

    1require 'swagger_helper'
    2
    3RSpec.describe 'users', type: :request do
    4  path '/users' do
    5    get('list users') do
    6      tags 'Users'
    7      produces 'application/json'
    8
    9      response(200, 'successful') do
    10        let!(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    11        schema type: :array, items: { '$ref' => '#/components/schemas/user_response' }
    12
    13        after do |example|
    14          example.metadata[:response][:content] = {
    15            'application/json' => {
    16              example: JSON.parse(response.body, symbolize_names: true)
    17            }
    18          }
    19        end
    20        run_test!
    21      end
    22    end
    23
    24    post('create user') do
    25      tags 'Users'
    26      consumes 'application/json'
    27      parameter name: :user, in: :body, schema: { '$ref' => '#/components/schemas/user_request_body' }
    28
    29      response(201, 'successful') do
    30        let(:user) { { user: { name: 'Test User', email: 'test@example.com' } } }
    31
    32        after do |example|
    33          example.metadata[:response][:content] = {
    34            'application/json' => {
    35              example: JSON.parse(response.body, symbolize_names: true)
    36            }
    37          }
    38        end
    39        run_test!
    40      end
    41    end
    42  end
    43
    44  path '/users/{id}' do
    45    parameter name: 'id', in: :path, type: :string, description: 'User ID'
    46
    47    get('show user') do
    48      tags 'Users'
    49      produces 'application/json'
    50
    51      response(200, 'successful') do
    52        let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    53        let(:id) { existing_user.id }
    54        schema '$ref' => '#/components/schemas/user_response'
    55
    56        after do |example|
    57          example.metadata[:response][:content] = {
    58            'application/json' => {
    59              example: JSON.parse(response.body, symbolize_names: true)
    60            }
    61          }
    62        end
    63        run_test!
    64      end
    65    end
    66
    67    patch('update user') do
    68      tags 'Users'
    69      consumes 'application/json'
    70      produces 'application/json'
    71      parameter name: :user, in: :body, schema: { '$ref' => '#/components/schemas/user_request_body' }
    72
    73      response(200, 'successful') do
    74        let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    75        let(:id) { existing_user.id }
    76        let(:user) { { user: { name: 'Updated User', email: 'updated@example.com' } } }
    77        schema '$ref' => '#/components/schemas/user_response'
    78
    79        after do |example|
    80          example.metadata[:response][:content] = {
    81            'application/json' => {
    82              example: JSON.parse(response.body, symbolize_names: true)
    83            }
    84          }
    85        end
    86        run_test!
    87      end
    88    end
    89
    90    put('update user') do
    91      tags 'Users'
    92      consumes 'application/json'
    93      produces 'application/json'
    94      parameter name: :user, in: :body, schema: { '$ref' => '#/components/schemas/user_request_body' }
    95
    96      response(200, 'successful') do
    97        let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    98        let(:id) { existing_user.id }
    99        let(:user) { { user: { name: 'Updated User', email: 'updated@example.com' } } }
    100        schema '$ref' => '#/components/schemas/user_response'
    101
    102        after do |example|
    103          example.metadata[:response][:content] = {
    104            'application/json' => {
    105              example: JSON.parse(response.body, symbolize_names: true)
    106            }
    107          }
    108        end
    109        run_test!
    110      end
    111    end
    112
    113    delete('delete user') do
    114      tags 'Users'
    115
    116      response(204, 'no content') do
    117        let(:existing_user) { User.create!(name: 'Test User', email: 'test@example.com') }
    118        let(:id) { existing_user.id }
    119
    120        run_test!
    121      end
    122    end
    123  end
    124end

    ドキュメント生成(完成系)

    ドキュメント生成コマンドを実行します。

    1rake rswag:specs:swaggerize RSWAG_DRY_RUN=0

    RSWAG_DRY_RUN=0としているのは、以下のafterブロックがこのオプションを追加しないと反映されないためになります。
    afterブロックは、レスポンス例をUIに表示する機能です。
    「response.body」をexampleに追加することで、spec内でAPIを実行した結果がUI上のレスポンス例として表示されるようになります。

    1after do |example|
    2  example.metadata[:response][:content] = {
    3    'application/json' => {
    4      example: JSON.parse(response.body, symbolize_names: true)
    5    }
    6  }
    7end

    再度ドキュメントにアクセスし、表示を確認します。
    http://localhost:3000/api-docs

    レスポンスやリクエストパラメーターが例付きで定義されていることが確認できます。

    作成したコンポーネントもUI上で確認することができます。

    最後にrspecを実行してパスすればOKです。
    スキーマと実際の挙動が同期していることが確認できました。

    1$ rspec
    2......
    3
    4Finished in 0.10116 seconds (files took 0.87154 seconds to load)
    56 examples, 0 failures

    最後に

    今回Swaggerを使用したAPIドキュメントの生成にトライしてみましたが、思った以上に大変でした。
    OpenAPI仕様に関して、まだまだ覚えることがたくさんあり学習コストはそれなりにありそうですが、導入することで 以下のようなメリットがあるかと思います。

    • APIの仕様を視覚的に確認できる
    • ドキュメントの自動生成
    • フロントエンドで型に沿った開発ができる

    今後、REST APIの開発案件にアサインされたら積極的に導入していこうかと思います。
    まだ使用されていない方は、ぜひ導入してみてはいかがでしょうか?
    ご愛読ありがとうございました。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    はっと(エンジニア)
    はっと(エンジニア)
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background