RailsでOpenAPIを使ったコントローラー作成入門:Design-firstアプローチでGET・POST・PUT・DELETE対応
はじめに
Design-firstアプローチとは、実装の前にAPIの仕様(OpenAPI YAML)を先に定義し、その仕様に合わせてコントローラーを実装する方法です。
- フロントエンドとバックエンドで仕様を共有しやすい
- APIの仕様と実装のズレを
committeegem で自動検出できる - 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入門:セットアップと基本コマンドの使い方」も参照してください。