RESTful API の設計原則
ナツミの最初のコードレビュー
APIの実装を始めたナツミ。まず物件(Property)のエンドポイントを作った。
# ナツミの最初の実装
Rails.application.routes.draw do
get '/getProperties', to: 'properties#index'
get '/getPropertyById/:id', to: 'properties#show'
post '/createProperty', to: 'properties#create'
post '/updateProperty/:id', to: 'properties#update'
post '/deleteProperty/:id', to: 'properties#destroy'
endレビューを依頼するとシニアエンジニアの田中さんから即座にコメントが来た。
「これ、RESTful じゃないね。全部直してほしい」
ナツミは頭を抱えた。「REST って何が正解なの?」
REST の6つの原則
REST(Representational State Transfer)はRoy Fieldingが2000年の論文で定義したアーキテクチャスタイル。6つの制約からなる。
APIエンジニアとして最重要なのは統一インターフェース——特に「リソース指向」と「HTTPメソッドの正しい使い方」だ。
リソース指向の命名規則
RESTの核心は「動詞ではなく名詞でURLを設計する」こと。
NG パターン(動詞URL)
GET /getProperties
POST /createProperty
POST /deleteProperty/1
OK パターン(リソースURL)
GET /properties # 一覧
POST /properties # 作成
GET /properties/1 # 詳細
PATCH /properties/1 # 更新
DELETE /properties/1 # 削除
INFO
URLはリソース(名詞)を表す。操作はHTTPメソッドで表現する。/deleteProperty ではなく DELETE /properties/1 が正解。
ネストしたリソース
物件(property)に属する部屋(room)のような関係は、URLで階層を表現する。
GET /properties/1/rooms # 物件1の部屋一覧
POST /properties/1/rooms # 物件1に部屋を追加
GET /properties/1/rooms/5 # 物件1の部屋5の詳細
WARNING
ネストは2階層まで。/properties/1/rooms/5/amenities/3/photos/2 のような深いネストはURLが複雑になりすぎる。3階層以上は設計を見直す。
HTTPメソッドの正しい使い方
| メソッド | 意味 | 冪等性 | 安全性 |
|---|---|---|---|
| GET | 取得 | ○ | ○ |
| POST | 作成 | ✗ | ✗ |
| PUT | 全体更新 | ○ | ✗ |
| PATCH | 部分更新 | ✗ | ✗ |
| DELETE | 削除 | ○ | ✗ |
冪等性:同じリクエストを何度送っても結果が変わらない。 安全性:サーバーの状態を変更しない。
PUT vs PATCH
// 既存リソース
{
"id": 1,
"name": "渋谷マンション",
"price": 150000,
"area": 35.5
}
// PUT(全体を置き換える)
// 省略したフィールドはnullになる
PUT /properties/1
{ "name": "渋谷マンション改", "price": 160000 }
// → area が null に!
// PATCH(指定したフィールドだけ更新)
PATCH /properties/1
{ "price": 160000 }
// → name, area は変わらずRailsでのルーティング設計
ナツミは正しい実装に書き直した。
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :properties do
resources :rooms, only: [:index, :create, :show, :update, :destroy]
member do
post :publish # 非標準アクション
delete :unpublish
end
collection do
get :featured # コレクションアクション
end
end
resources :users, only: [:show, :update]
resource :session, only: [:create, :destroy] # 単数リソース
end
end
end生成されるルートを確認する。
$ rails routes | grep properties
GET /api/v1/properties api/v1/properties#index
POST /api/v1/properties api/v1/properties#create
GET /api/v1/properties/:id api/v1/properties#show
PATCH /api/v1/properties/:id api/v1/properties#update
PUT /api/v1/properties/:id api/v1/properties#update
DELETE /api/v1/properties/:id api/v1/properties#destroy
POST /api/v1/properties/:id/publish api/v1/properties#publish
GET /api/v1/properties/featured api/v1/properties#featuredAPIコントローラの実装
# app/controllers/api/v1/properties_controller.rb
module Api
module V1
class PropertiesController < Api::V1::BaseController
before_action :set_property, only: [:show, :update, :destroy, :publish]
def index
@properties = Property
.where(published: true)
.order(created_at: :desc)
.page(params[:page])
.per(params[:per_page] || 20)
render json: {
data: @properties.map { |p| property_json(p) },
meta: pagination_meta(@properties)
}
end
def show
render json: { data: property_json(@property) }
end
def create
@property = current_user.properties.build(property_params)
if @property.save
render json: { data: property_json(@property) }, status: :created
else
render json: { errors: @property.errors.full_messages },
status: :unprocessable_entity
end
end
def update
if @property.update(property_params)
render json: { data: property_json(@property) }
else
render json: { errors: @property.errors.full_messages },
status: :unprocessable_entity
end
end
def destroy
@property.destroy
head :no_content
end
private
def set_property
@property = Property.find(params[:id])
end
def property_params
params.require(:property).permit(:name, :description, :price, :area, :prefecture, :city)
end
def property_json(property)
{
id: property.id,
name: property.name,
price: property.price,
area: property.area,
location: "#{property.prefecture}#{property.city}",
created_at: property.created_at.iso8601
}
end
end
end
endHTTPステータスコードの使い方
適切なステータスコードを返すことで、クライアントはレスポンスの意味を正確に把握できる。
# よく使うステータスコード
# 2xx 成功
render json: data, status: :ok # 200 GET成功
render json: data, status: :created # 201 作成成功
head :no_content # 204 削除成功(本文なし)
# 4xx クライアントエラー
render json: errors, status: :bad_request # 400 リクエスト不正
render json: errors, status: :unauthorized # 401 認証失敗
render json: errors, status: :forbidden # 403 権限なし
render json: errors, status: :not_found # 404 リソース不存在
render json: errors, status: :unprocessable_entity # 422 バリデーション失敗
# 5xx サーバーエラー
render json: errors, status: :internal_server_error # 500 サーバーエラーWARNING
401 vs 403の違い:401は「誰なのか不明(未認証)」、403は「誰なのかはわかるが権限がない(認可失敗)」。ログインしていないユーザーには401、ログイン済みだが権限がない場合は403を返す。
クエリパラメータの設計
フィルタリング、ソート、ページネーションはクエリパラメータで実装する。
# GET /api/v1/properties?prefecture=東京都&min_price=100000&sort=price_asc&page=2
def index
@properties = Property.where(published: true)
# フィルタリング
@properties = @properties.where(prefecture: params[:prefecture]) if params[:prefecture]
@properties = @properties.where('price >= ?', params[:min_price]) if params[:min_price]
@properties = @properties.where('price <= ?', params[:max_price]) if params[:max_price]
# ソート
case params[:sort]
when 'price_asc' then @properties = @properties.order(price: :asc)
when 'price_desc' then @properties = @properties.order(price: :desc)
when 'newest' then @properties = @properties.order(created_at: :desc)
else @properties = @properties.order(created_at: :desc)
end
# ページネーション
@properties = @properties.page(params[:page]).per(20)
endベースコントローラの設定
API専用の基底コントローラを作り、共通処理をまとめる。
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_user!
rescue_from ActiveRecord::RecordNotFound do |e|
render json: { error: 'Not found' }, status: :not_found
end
private
def current_user
@current_user ||= authenticate_token
end
def authenticate_user!
render json: { error: 'Unauthorized' }, status: :unauthorized unless current_user
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count,
per_page: collection.limit_value
}
end
end
end
endまとめ
- RESTはURLを**名詞(リソース)**で設計し、操作はHTTPメソッドで表現する
resourcesを使ったRailsルーティングで標準的なCRUDが簡潔に書ける- ネストは2階層まで、非標準アクションは
member/collectionで定義する - HTTPステータスコードを適切に使うことでクライアントが正確に状態を把握できる
- APIコントローラは
ActionController::APIを継承し、共通処理はBaseControllerに集約する
次章では、コントローラが返すJSONの形を整えるシリアライゼーションを学ぶ。jbuilder、ActiveModelSerializers、JSON:APIの違いを理解しよう。