APIをREST設計で作成する中でURIをネストさせますが、
そのまま作成していると
Controllerフォルダがすごいことに....
Controllerフォルダも階層分けして見やすくするか...
と考え実装すると
- URIはネストできたが、Controllerのファイルがネストされない?
- Controllerのファイルはネストしたが、URIが変わらない?
- URIだけ名前を変えたいんだけど...
となり、カオスな状態に。
...一度整理しましょう。
まずREST設計とは?
APIを作成するなら、デファクトになりつつあるREST設計
ざっくりルール化するとすれば
- なるべくシンプルに
- 全てのリソースがURIで示せる
- URIは基本名詞で(動詞やメソッド名は❌)
- GET、POST、PUT、PATCH、DELETEを基本とする。
- バージョニング情報(v1等)をURIに含める
あたりでしょうか。
カスタムメソッドは認めないとか
HATEOASの実装とかは触れないことにします。
参考になったのはこの辺り
実例:こんなURIはRESTfulとは認めない!
railsの場合は、従ってだけでRESTfulな設計になります。
例えば、
routeファイルでresourceをするだけで
resources :users
6つのアクションが自動生成されますね。
これはREST設計になっています。
GET /users users#index
POST /users users#create
GET /users/:id users#show
PATCH /users/:id users#update
PUT /users/:id users#update
DELETE /users/:id users#destroy
さてここで、もう少し深掘りしてみましょう。
仮に
- usersテーブル
- usersと多対多の関係にあるclubsテーブル
- その中間テーブルであるusers_clubsテーブル
があるとします。
id=2のuserが所属しているclub
を表すURIは
/users/users_clubs/2
となりますね....
$\huge{.....アウト!}$
イケてないURIですね。
修正版はこちら
/users/users_clubs/2 ❌
/v1/users/2/clubs/ ✅
イケてない点としては
- user_idを参照しているなら、users/:user_id/clubs とするべき
- users_clubの前にusersがあるため、userがもつclub情報なら、users_culbsとするのは冗長(clubsで良い)
- 修正前のURIのみを見た場合、club_idと勘違いするリスクがある
- バージョニング(V1)が含まれていない
あたりでしょうか。
それでは1つ1つ直していきましょう。
### namespaceを使用して、バージョニング情報の付与
Controller配下にV1フォルダを作成し
app/controllers/V1/user_controller.rb
となるように配置しましょう。
このようにすることで、今後APIの改修が入った際に
v2としてディレクトリも分けることが可能ですし、
URIも分けることができます。
コントローラーの配置はこのような形になります。
controller
┗ V1
┗ user.controller
route.rb側では
namespaceを使用することで、
URIもコントローラもネストすることができます
namespace :V1 do
resources :users
end
rake routes
で確認してもURIに
綺麗にV1が入っていて、無事バージョニング情報を入れることができました。
# rake routes
GET /V1/users(.:format)
POST /V1/users(.:format)
GET /V1/users/:id(.:format)
PATCH /V1/users/:id(.:format)
PUT /V1/users/:id(.:format)
DELETE /V1/users/:id(.:format)
バージョニング関係は、色々なやり方があります。
gemならversionist
などなど...
さて、user_culbsも追加していきましょう
memberを使い、idを伴うURIを追加する
user:idを引き継いでuser_clubsを呼び出したいので
memberを使います
namespace :V1, format: 'json' do
resources :users do
member do
get :user_clubs
post :user_clubs
end
end
end
これで
V1/users/:id/user_clubsと
user_idを渡す形で作成できました。
Prefix Verb URI Pattern Controller#Action
user_clubs_V1_user GET /V1/users/:id/user_clubs(.:format) V1/users#user_clubs {:format=>/json/}
POST /V1/users/:id/user_clubs(.:format) V1/users#user_clubs {:format=>/json/}
V1_users GET /V1/users(.:format) V1/users#index {:format=>/json/}
POST /V1/users(.:format) V1/users#create {:format=>/json/}
V1_user GET /V1/users/:id(.:format) V1/users#show {:format=>/json/}
PATCH /V1/users/:id(.:format) V1/users#update {:format=>/json/}
PUT /V1/users/:id(.:format) V1/users#update {:format=>/json/}
DELETE /V1/users/:id(.:format) V1/users#destroy {:format=>/json/}
idを入れたくない場合は、collection
これとは逆に、idを間に入れたくない場合は
collectionを使います。
userコントローラなどで、ファイルを分けずにカスタムアクションとして追加したい場合に使用します
namespace :V1, format: 'json' do
resources :users do
collection do
get 'name', controller: :users, action: :name
end
end
end
今回はこれは使用しません。
toオプションを使い、controllerとActionを指定
先ほどのrake routes
の右端、Controller#Actionを見ると
v1/users#user_clubsとなっており、
user_controller.rbのuser_clubsアクションが指定されています。
中間テーブルを扱うので、
Controllerは分けたいところですね。
むしろそうしないと、
user_controllerが長くなってしまいます。
menberの中をtoアクションで
user_clubs_controllerのindexアクションを向くように指定します。
namespace :v1, format: 'json' do
resources :users do
member do
get 'user_clubs', to: 'user_clubs#index'
post 'user_clubs', to: 'user_clubs#create'
end
end
end
Prefix Verb URI Pattern Controller#Action
user_clubs_V1_user GET /V1/users/:id/user_clubs(.:format) V1/user_clubs#index {:format=>/json/}
POST /V1/users/:id/user_clubs(.:format) V1/user_clubs#create {:format=>/json/}
V1_users GET /V1/users(.:format) V1/users#index {:format=>/json/}
POST /V1/users(.:format) V1/users#create {:format=>/json/}
V1_user GET /V1/users/:id(.:format) V1/users#show {:format=>/json/}
PATCH /V1/users/:id(.:format) V1/users#update {:format=>/json/}
PUT /V1/users/:id(.:format) V1/users#update {:format=>/json/}
DELETE /V1/users/:id(.:format) V1/users#destroy {:format=>/json/}
V1/user_clubs#indexとなり、
中間テーブルのuser_clubs_controller.rbを向いていますね。
順調順調。
ここで、もう一工夫。URIのuser_clubsを変更したい
もう一工夫加えましょう。
冒頭で話した通り、userの所属するclubなら
user_clubsとするのは冗長です。
URIを
V1/users/:id/user_clubsから
V1/users/:id/clubsに変えたいですね。
URIのネスト変更ではなく、名前を変えるだけなら
アクション以下を
get 'user_clubs'から
get 'clubs'へ変更するだけで済みます。
namespace :V1, format: 'json' do
resources :users do
member do
get 'clubs', to: 'user_clubs#index'
post 'clubs', to: 'user_clubs#create'
end
end
end
Prefix Verb URI Pattern Controller#Action
clubs_V1_user GET /V1/users/:id/clubs(.:format) V1/user_clubs#index {:format=>/json/}
POST /V1/users/:id/clubs(.:format) V1/user_clubs#create {:format=>/json/}
V1_users GET /V1/users(.:format) V1/users#index {:format=>/json/}
POST /V1/users(.:format) V1/users#create {:format=>/json/}
V1_user GET /V1/users/:id(.:format) V1/users#show {:format=>/json/}
PATCH /V1/users/:id(.:format) V1/users#update {:format=>/json/}
PUT /V1/users/:id(.:format) V1/users#update {:format=>/json/}
DELETE /V1/users/:id(.:format) V1/users#destroy {:format=>/json/}
URI Patternが
/V1/users/:id/clubs
になっていますね。
スマートです。
Controllerをネストしたいなら、scope module、URIだけをネストしたいならscope
さて、ここまでであらかたRESTfulなURIは作れました。
しかしこのままでは悲劇が待っています。
controllerを階層化するのが終わってないからですね。
RESTfulな設計をすればするほど、
controllerの数は増える傾向にありますが、
このままではcontroller/V1配下に大量のコントローラが並べられることになります。
階層を作ってネストさせましょう。
URIを変更せず、Controllerのファイル階層だけを変更するには
scope moduleを使用します。
(逆に、URIをネストさせ、controllerを変更しない場合は、scopeを使用します。)
今回は、V1配下にusersフォルダを作成し、
中間テーブル(user_clubs)を入れることにします。
namespace :V1, format: 'json' do
resources :users do
scope module: :users do
member do
get 'clubs', to: 'users_clubs#index'
post 'clubs', to: 'users_clubs#create'
end
end
end
end
Prefix Verb URI Pattern Controller#Action
clubs_V1_user GET /V1/users/:id/clubs(.:format) V1/users/users_clubs#index {:format=>/json/}
POST /V1/users/:id/clubs(.:format) V1/users/users_clubs#create {:format=>/json/}
V1_users GET /V1/users(.:format) V1/users#index {:format=>/json/}
POST /V1/users(.:format) V1/users#create {:format=>/json/}
V1_user GET /V1/users/:id(.:format) V1/users#show {:format=>/json/}
PATCH /V1/users/:id(.:format) V1/users#update {:format=>/json/}
PUT /V1/users/:id(.:format) V1/users#update {:format=>/json/}
DELETE /V1/users/:id(.:format) V1/users#destroy {:format=>/json/}
Controller#Actionが
V1/users/user_clubs#index と
users配下のuser_clubsに向いていますね!
これでコントローラファイルも、良い感じにネストさせることができました
controllerがごちゃごちゃになるのを防げそうです。
最終形はこんな感じ
controller
┗ V1
┣ users_controller.rb
┗ user
┗ users_clubs_controller.rb
お題の、userがもつclub情報の取得もこのURIでできそうです。
v1/users/:id/clubs
URIだけREST設計っぽく作るなら苦労しなかったんですが、
Controllerのファイルをネストさせたり
URIだけ変更したりするうちに、混ざって詰まりました。
各処理の役割と、書く順番は気をつけましょうというお話です。
追記:別の書き方
controllerで使用するcontrollerファイルを
moduleでフォルダを指定すれば、
こんな感じでも書けますね。
ブロック使わない書き方なので、シンプル。
namespace :V1 do
resources :users do
resources :clubs, controller: :user_clubs, module: :users, only: %i[index create]
end
end
最後に、ざっくりまとめ
- 「namespace」は、URIもControllerの格納フォルダも指定する
- 「member」は、idを引き継いだURI作成
- 「collection」は、id引き継がないURI作成
- 「to」オプションで、controllerとアクションの向き先を変更可能
- URIのみネストさせたいときは、「scope」
- controllerの格納フォルダをネストさせるなら、「scope module」
参照リンク集
Shallowは、賛否両論あるみたい。
resources を nest するときは shallow を使うと幸せになれる - Qiita
RESTの思想が汚れるという問題もあるようで