- 3.1 REST in a Rather Small Nutshell
- 3.2 Resources and Representations
- 3.3 REST in Rails
- 3.4 Routing and CRUD
- 3.5 The Standard RESTful Controller Actions
- 3.6 Singular Resource Routes
- 3.7 Nested Resources
- 3.8 RESTful Route Customizations
- 3.9 Controller-Only Resources
- 3.10 Different Representations of Resources
- 3.11 The RESTful Rails Action Set
- 3.12 Conclusion
3.8 RESTful Route Customizations
Rails's RESTful routes give you a pretty nice package of named routes, mapped to useful, common, controller actions—the CRUD superset you've already learned about. Sometimes, however, you want to customize things a little more, while still taking advantage of the RESTful route naming conventions and the multiplication table approach to mixing named routes and HTTP request methods.
The techniques for doing this are useful when, for example, you've got more than one way of viewing a resource that might be described as showing. You can't (or shouldn't) use the show action itself for more than one such view. Instead, you need to think in terms of different perspectives on a resource, and create URLs for each one.
3.8.1 Extra Member Routes
For example, let's say we want to make it possible to retract a bid. The basic nested route for bids looks like this:
resources :auctions do resources :bids end
We'd like to have a retract action that shows a form (and perhaps does some screening for retractability). The retract isn't the same as destroy; it's more like a portal to destroy. It's similar to edit, which serves as a form portal to update. Following the parallel with edit/update, we want a URL that looks like
/auctions/3/bids/5/retract
and a helper method called retract_auction_bid_url. The way you achieve this is by specifying an extra member route for the bids, as in Listing 3.1
Listing 3.1. Adding an extra member route
resources :auctions do resources :bids do member do get :retract end end end
Then you can add a retraction link to your view using
link_to "Retract", retract_bid_path(auction, bid)
and the URL generated will include the /retract modifier. That said, you should probably let that link pull up a retraction form (and not trigger the retraction process itself!). The reason I say that is because, according to the tenets of HTTP, GET requests should not modify the state of the server; that's what POST requests are for.
So how do you trigger an actual retraction? Is it enough to add a :method option to link_to?
link_to "Retract", retract_bid_path(auction,bid), :method => :post
Not quite. Remember that in Listing 3.1 we defined the retract route as a get, so a POST will not be recognized by the routing system. The solution is to define an extra member route with post, like this:
resources :auctions do resources :bids do member do get :retract post :retract end end end
If you're handling more than one HTTP verb with a single action, you should switch to using a single match declaration and a :via option, like this:
resources :auctions do resources :bids do member do match :retract, :via => [:get, :post] end end end
Thanks to the flexibility of the routing system, we can tighten it up further using match with an :on option, like
resources :auctions do resources :bids do match :retract, :via => [:get, :post], :on => :member end end
which would result in a route like this (output from rake routes):
retract_auction_bid GET|POST /auctions/:auction_id/bids/:id/retract(.:format) {:controller => "bids", :action => "retract"}
3.8.2 Extra Collection Routes
You can use the same routing technique to add routes that conceptually apply to an entire collection of resources:
resources :auctions do collection do match :terminate, :via => [:get, :post] end end
In its shorter form:
resources :auctions do match :terminate, :via => [:get, :post], :on => :collection end
This example will give you a terminate_auctions_path method, which will produce a URL mapping to the terminate action of the auctions controller. (A slightly bizarre example, perhaps, but the idea is that it would enable you to end all auctions at once.)
Thus you can fine-tune the routing behavior—even the RESTful routing behavior—of your application, so that you can arrange for special and specialized cases while still thinking in terms of resources.
3.8.3 Custom Action Names
Occasionally, you might want to deviate from the default naming convention for Rails RESTful routes. The :path_names option allows you to specify alternate name mappings. The example code shown changes the new and edit actions to Spanish-language equivalents.
resources :projects, :path_names => { :new => 'nuevo', :edit => 'cambiar'}
The URLs change (but the names of the generated helper methods do not).
new_report GET /reports/nuevo(.:format) edit_report GET /reports/:id/cambiar(.:format)
3.8.4 Mapping to a Different Controller
You may use the :controller option to map a resource to a different controller than the one it would do so by default. This feature is occasionally useful for aliasing resources to a more natural controller name.
resources :photos, :controller => "images"
3.8.5 Routes for New Resources
The routing system has a neat syntax for specifying routes that only apply to new resources, ones that haven't been saved yet. You declare extra routes inside of a nested new block, like this:
resources :reports do new do post :preview end end
The declaration above would result in the following route being defined.
preview_new_report POST /reports/new/preview(.:format) {:action=>"preview", :controller=>"reports"}
Refer to your new route within a view form by altering the default :url.
= form_for(report, :url => preview_new_report_path) do |f| ... = f.submit "Preview"
3.8.6 Considerations for Extra Routes
Referring to extra member and collection actions, David has been quoted as saying, "If you're writing so many additional methods that the repetition is beginning to bug you, you should revisit your intentions. You're probably not being as RESTful as you could be."
The last sentence is key. Adding extra actions corrupts the elegance of your overall RESTful application design, because it leads you away from finding all of the resources lurking in your domain.
Keeping in mind that real applications are more complicated than code examples in a reference book, let's see what would happen if we had to model retractions strictly using resources. Rather than tacking a retract action onto the BidsController, we might feel compelled to introduce a retraction resource, associated with bids, and write a RetractionController to handle it.
resources :bids do resource :retraction end
RetractionController could now be in charge of everything having to do with retraction activities, rather than having that functionality mixed into BidsController. And if you think about it, something as weighty as bid retraction would eventually accumulate quite a bit of logic. Some would call breaking it out into its own controller proper separation of concerns or even just good object-orientation.