RailsでOpenAPIを使ったコントローラー作成入門:Design-firstアプローチでGET・POST・PUT・DELETE対応

スポンサーリンク

RailsでOpenAPIを使ったコントローラー作成入門:Design-firstアプローチでGET・POST・PUT・DELETE対応

はじめに

Design-firstアプローチとは、実装の前にAPIの仕様(OpenAPI YAML)を先に定義し、その仕様に合わせてコントローラーを実装する方法です。

  • フロントエンドとバックエンドで仕様を共有しやすい
  • APIの仕様と実装のズレを committee gem で自動検出できる
  • YAMLがそのままAPIドキュメントになる

この記事ではOpenAPI YAMLの書き方からRailsコントローラーの実装、committeeによる検証までを解説します。


セットアップ

Gemfile に追加します。

gem 'committee'

インストールします。

bundle install

OpenAPI YAMLを書く

docs/openapi.yml にAPI仕様を定義します。ユーザー一覧取得・作成の例です。

openapi: "3.0.3"
info:
  title: MyApp API
  version: "1.0"

paths:
  /api/users:
    get:
      summary: ユーザー一覧取得
      operationId: getUsers
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/User"

    post:
      summary: ユーザー作成
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: 作成成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "422":
          description: バリデーションエラー
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/users/{id}:
    get:
      summary: ユーザー詳細取得
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "404":
          description: 見つからない
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    put:
      summary: ユーザー更新
      operationId: updateUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateUserRequest"
      responses:
        "200":
          description: 更新成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "404":
          description: 見つからない
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: バリデーションエラー
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

    delete:
      summary: ユーザー削除
      operationId: deleteUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "204":
          description: 削除成功(ボディなし)
        "404":
          description: 見つからない
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

components:
  schemas:
    User:
      type: object
      required:
        - id
        - name
        - email
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string

    CreateUserRequest:
      type: object
      required:
        - name
        - email
      properties:
        name:
          type: string
        email:
          type: string
          format: email

    UpdateUserRequest:
      type: object
      properties:
        name:
          type: string
        email:
          type: string
          format: email

    ErrorResponse:
      type: object
      required:
        - errors
      properties:
        errors:
          type: array
          items:
            type: string

YAMLの基本構造

キー 説明
paths エンドポイントの定義
components/schemas 使い回す型の定義
$ref 定義を参照する
requestBody リクエストボディのスキーマ
responses レスポンスのスキーマ

routesを書く

YAMLの paths に対応するルーティングを定義します。

# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    resources :users, only: [:index, :show, :create, :update, :destroy]
  end
end

コントローラーを実装する

app/controllers/api/users_controller.rb を作成します。

module Api
  class UsersController < ApplicationController
    def index
      users = User.all
      render json: users.map { |u| user_json(u) }
    end

    def show
      user = User.find(params[:id])
      render json: user_json(user)
    rescue ActiveRecord::RecordNotFound
      render json: { errors: ['ユーザーが見つかりません'] }, status: :not_found
    end

    def create
      user = User.new(user_params)
      if user.save
        render json: user_json(user), status: :created
      else
        render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
      end
    end

    def update
      user = User.find(params[:id])
      if user.update(user_params)
        render json: user_json(user)
      else
        render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
      end
    rescue ActiveRecord::RecordNotFound
      render json: { errors: ['ユーザーが見つかりません'] }, status: :not_found
    end

    def destroy
      user = User.find(params[:id])
      user.destroy
      head :no_content
    rescue ActiveRecord::RecordNotFound
      render json: { errors: ['ユーザーが見つかりません'] }, status: :not_found
    end

    private

    def user_params
      params.require(:user).permit(:name, :email)
    end

    def user_json(user)
      { id: user.id, name: user.name, email: user.email }
    end
  end
end

committeeでリクエスト・レスポンスを検証する

committeeをRailsのミドルウェアとして設定することで、OpenAPIの仕様に合わないリクエストやレスポンスを自動で検出できます。

# config/application.rb
module MyApp
  class Application < Rails::Application
    config.middleware.use(
      Committee::Middleware::RequestValidation,
      schema_path: Rails.root.join('docs/openapi.yml').to_s,
      prefix: '/api',
      strict: false,
      parse_response_by_content_type: true
    )
  end
end

オプションの説明

オプション 説明
schema_path OpenAPI YAMLファイルのパス
prefix 検証対象のパスプレフィックス
strict true にするとスキーマ未定義のエンドポイントへのアクセスを拒否

検証の動作確認

仕様に合わないリクエストを送ると400エラーが返ります。

# email なしで POST(requireになっている)
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "田中太郎"}'

# → 400 Bad Request(committeeが検出)
# {"message":"#/components/schemas/CreateUserRequest missing required parameters: email"}

正しいリクエストは通ります。

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"user": {"name": "田中太郎", "email": "tanaka@example.com"}}'

# → 201 Created

レスポンスの検証も追加する(開発環境のみ)

開発環境ではレスポンスの形式もチェックできます。

# config/environments/development.rb
config.middleware.use(
  Committee::Middleware::ResponseValidation,
  schema_path: Rails.root.join('docs/openapi.yml').to_s
)

コントローラーがYAMLのレスポンス定義と異なる形式を返したとき、エラーを検出してくれます。


まとめ

HTTPメソッド アクション 説明
GET index 一覧取得
GET show 詳細取得
POST create 作成(201)
PUT update 更新(200)
DELETE destroy 削除(204 ボディなし)
ステップ 内容
1. YAML定義 docs/openapi.yml にpaths・schemasを定義する
2. routes YAMLのpathsに対応したルーティングを書く
3. controller YAMLのスキーマに合ったJSONを返す実装をする
4. committee ミドルウェアとして設定してリクエスト・レスポンスを自動検証

Design-firstアプローチはフロントエンドとAPIの仕様を先に合意してから実装できるため、並行開発がしやすくなります。committeeを入れることでYAMLと実装のズレを早期に検出できます。

デバッグには「Rails binding.pry入門:セットアップと基本コマンドの使い方」も参照してください。