18
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rails APIでREST設計しようとすると、ControllerとURIをうまくネストされない!

Last updated at Posted at 2019-10-03

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をするだけで

config/route.rb
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もコントローラもネストすることができます

config/route.rb
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)       

バージョニング関係は、色々なやり方があります。

#350 REST API Versioning

gemならversionist

などなど...



さて、user_culbsも追加していきましょう

memberを使い、idを伴うURIを追加する

user:idを引き継いでuser_clubsを呼び出したいので
memberを使います

config/route.rb
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コントローラなどで、ファイルを分けずにカスタムアクションとして追加したい場合に使用します

config/route.rb
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アクションを向くように指定します。

config/route.rb
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'へ変更するだけで済みます。

config/route.rb
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)を入れることにします。

config/route.rb
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でフォルダを指定すれば、
こんな感じでも書けますね。

ブロック使わない書き方なので、シンプル。

config/route.rb
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の思想が汚れるという問題もあるようで

Railsのroutesのshallowは安易に使わないで欲しい – Matsubo

18
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?