api-versioning について
Rails の API バージョニングを扱う開発者向けスキルです。バージョン指定、コントローラー、応答形式、認証、リクエストスペックを取り上げ、互換性を保ちながら API を更新する構成を示します。
主な機能
- 01URL、クエリ、ヘッダーによるバージョン指定
- 02Rails のバージョン別ルートとコントローラー
- 03JSON 応答とエラー形式
- 04トークン認証と JWT の例
- 05リクエストスペックのテンプレート
活用シーン
- 01Rails API のバージョニング方針の策定
- 02複数 API バージョン向けコントローラーの構成
- 03API 応答とエラー形式の統一
- 04API エンドポイントのリクエストスペック作成
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ thibautbaissac/ rails_ai_agents --skill api-versioningインストール後、Claude Code で/api-versioningを入力して読み込まれたか確認します。
詳細
Rails の API バージョニング
概要
適切に構成された API では、後方互換性と明確な整理のためにバージョニングが必要です。
バージョニング戦略
| 戦略 | URL の例 | ヘッダーの例 |
|---|---|---|
| URL パス | /api/v1/users | - |
| クエリパラメーター | /api/users?version=1 | - |
| ヘッダー | /api/users | Accept: application/vnd.api+json; version=1 |
| Accept ヘッダー | /api/users | Accept: application/vnd.myapp.v1+json |
推奨:URL パスによるバージョニング(最も一般的で、理解しやすい方法です)
クイックセットアップ
ルート
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users, only: [:index, :show, :create, :update, :destroy]
resources :posts, only: [:index, :show, :create]
end
# v2 with changes
namespace :v2 do
resources :users, only: [:index, :show, :create, :update, :destroy]
end
end
end
ディレクトリー構成
app/controllers/
├── api/
│ ├── base_controller.rb # Shared API logic
│ ├── v1/
│ │ ├── base_controller.rb # V1 base
│ │ ├── users_controller.rb
│ │ └── posts_controller.rb
│ └── v2/
│ ├── base_controller.rb # V2 base
│ └── users_controller.rb
ベースコントローラー
# app/controllers/api/base_controller.rb
module Api
class BaseController < ApplicationController
# Skip CSRF for API requests
skip_before_action :verify_authenticity_token
# Respond with JSON by default
respond_to :json
# Handle common errors
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
def unprocessable_entity(exception)
render json: { errors: exception.record.errors }, status: :unprocessable_entity
end
def bad_request(exception)
render json: { error: exception.message }, status: :bad_request
end
end
end
バージョンベースコントローラー
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < Api::BaseController
# V1-specific configuration
end
end
end
リソースコントローラー
# app/controllers/api/v1/users_controller.rb
module Api
module V1
class UsersController < BaseController
before_action :set_user, only: [:show, :update, :destroy]
def index
@users = User.page(params[:page]).per(25)
render json: {
data: @users,
meta: pagination_meta(@users)
}
end
def show
render json: { data: @user }
end
def create
@user = User.create!(user_params)
render json: { data: @user }, status: :created
end
def update
@user.update!(user_params)
render json: { data: @user }
end
def destroy
@user.destroy
head :no_content
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email)
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count
}
end
end
end
end
レスポンス形式
標準的な JSON レスポンス
{
"data": {
"id": 1,
"type": "user",
"attributes": {
"name": "John Doe",
"email": "[email protected]",
"created_at": "2024-01-15T10:30:00Z"
}
}
}
コレクションレスポンス
{
"data": [
{ "id": 1, "type": "user", "attributes": { ... } },
{ "id": 2, "type": "user", "attributes": { ... } }
],
"meta": {
"current_page": 1,
"total_pages": 10,
"total_count": 100
}
}
エラーレスポンス
{
"error": "Record not found",
"code": "not_found"
}
{
"errors": {
"email": ["has already been taken"],
"name": ["can't be blank"]
}
}
API のテスト
リクエストスペックのテンプレート
# spec/requests/api/v1/users_spec.rb
require 'rails_helper'
RSpec.describe 'Api::V1::Users', type: :request do
let(:headers) { { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } }
describe 'GET /api/v1/users' do
let!(:users) { create_list(:user, 3) }
it 'returns all users' do
get '/api/v1/users', headers: headers
expect(response).to have_http_status(:ok)
expect(json_response['data'].size).to eq(3)
end
it 'returns paginated results' do
get '/api/v1/users', params: { page: 1 }, headers: headers
expect(json_response['meta']).to include('current_page', 'total_pages')
end
end
describe 'GET /api/v1/users/:id' do
let(:user) { create(:user) }
it 'returns the user' do
get "/api/v1/users/#{user.id}", headers: headers
expect(response).to have_http_status(:ok)
expect(json_response['data']['id']).to eq(user.id)
end
context 'when user not found' do
it 'returns 404' do
get '/api/v1/users/999999', headers: headers
expect(response).to have_http_status(:not_found)
end
end
end
describe 'POST /api/v1/users' do
let(:valid_params) { { user: { name: 'Test', email: '[email protected]' } } }
it 'creates a user' do
expect {
post '/api/v1/users', params: valid_params.to_json, headers: headers
}.to change(User, :count).by(1)
expect(response).to have_http_status(:created)
end
context 'with invalid params' do
let(:invalid_params) { { user: { name: '', email: '' } } }
it 'returns validation errors' do
post '/api/v1/users', params: invalid_params.to_json, headers: headers
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response['errors']).to be_present
end
end
end
# Helper method
def json_response
JSON.parse(response.body)
end
end
API 認証
トークンベース認証
# app/controllers/api/base_controller.rb
module Api
class BaseController < ApplicationController
before_action :authenticate_api_user!
private
def authenticate_api_user!
token = request.headers['Authorization']&.split(' ')&.last
@current_api_user = User.find_by(api_token: token)
render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_api_user
end
def current_api_user
@current_api_user
end
end
end
JWT 認証
# Using jwt gem
def authenticate_api_user!
token = request.headers['Authorization']&.split(' ')&.last
return unauthorized unless token
payload = JWT.decode(token, Rails.application.secret_key_base).first
@current_api_user = User.find(payload['user_id'])
rescue JWT::DecodeError
unauthorized
end
def unauthorized
render json: { error: 'Unauthorized' }, status: :unauthorized
end
ワークフローチェックリスト
API Implementation:
- [ ] Define routes in namespace
- [ ] Create base controller with error handling
- [ ] Create version-specific base controller
- [ ] Create resource controller
- [ ] Add authentication (if needed)
- [ ] Write request specs
- [ ] Document API endpoints
コメント
Markdown 対応