203: Routing in Rails 3
(view original Railscast)
We’ll continue our look into Rails 3’s new features in this episode, this time concentrating on routing. Rails 3 has a new API for defining routes and some new features. To show how the new routing works we’ll create a new application called detour and create some routes for it.
rails detour
Once we’ve created the application we can open it up in a text editor to look at the config/routes.rb
file. The default file contains some documentation and examples of the new API and is worth reading through. We’re going to replace the default contents with an example of some old routes and show their Rails 3 equivalents.
/config/routes.rb
Detour::Application.routes.draw do |map| map.resources :products, :member => { :detailed => :get } map.resources :forums, :collection => { :sortable => :get, :sort => :put } do |forums| forums.resources :topics end map.root :controller => "home", :action => "index" map.about "/about", :controller => "info", :action => "about" end
The old-style routes that we’re going to convert.
We’ll start with the first route in the collection above:
map.resources :products, :member = { :detailed => :get }
This route has a products resource and a specific additional member action called detailed which is called via a GET request.
The first change to note in Rails 3’s routing is that we no longer deal with the map
object, instead calling resources
directly in the routes.draw
block. :member
and :collection
actions inside resources are defined inside a block. The route can therefore be rewritten like this:
resources :products do get :detailed, :on => :member end
Next we’ll look at a more complex route:
map.resources :forums, :collection => { :sortable => :get, :sort => :put } do |forums| forums.resources :topics end
In this route we have a forum resource with two additional collection items and a nested topics resource. With the new API this route can be written like this:
resources :forums do collection do get :sortable put :sort end resources :topics end
Again we use resources
instead of map.resources
and pass in a block. We have two collection actions in the route. While we could define those in the same way we did with the detailed action in the first route making use of the :on
argument instead we’ll define a collection
block (members can be treated the same way in a member
block) and any routes defined inside the block will act on the collection of forums. In our block we’ve defined two new actions sortable as a GET request and sort as a PUT.
For the nested topics resource we just call resources
again, nesting the topics resource inside forums.
The next route we’ll look at is the one that defines the controller and action that the root URL point to:
map.root :controller => "home", :action => "index"
Here we can just call root
and use a :to
argument to which we pass a string that contains the name of the controller and action separated by a hash.
root :to => "home#index"
This ability to specify a controller and action in a single string is a new feature of Rails 3. We can do something similar for a named route:
map.about "/about", :controller => "info", :action => "about"
With the Rails 3 API this can be rewritten like this:
match "/about" => "info#about", :as => :about
Without the :as
argument the route above would just be a generic route. Adding :as
makes it a named route so that we can use about_path
or about_url
in our application.
New Features
As you can see from the examples above it’s not difficult to convert routes from the old API to the new one but what really makes the new routing API interesting is the new features it provides and we’ll spend the rest of the episode covering some of them.
Optional Parameters
Optional parameters were supported in previous versions of Rails but the syntax was a little clumsy. Lets see how it’s done in Rails 3.
It will be easier to demonstrate optional parameters if our application has a controller so we’ll create an info
controller with an about action. Note that in Rails 3 we can use rails g
as a shortcut for rails generate
.
rails g controller info about
We can start up our server with another new shortcut:
rails s
Now, if we visit http://localhost:3000/about we’ll see the info#about
action as determined by the routes we wrote earlier.
With that working we want to provide a PDF version of this action but if we visit http://localhost:3000/about.pdf we’ll get a routing error as our application doesn’t know how to match the route.
In the routes file we can change the about
route to accept a format parameter by adding a full stop and :format
:
match "/about.:format" => "info#about", :as => :about
If we reload PDF view now the route will be matched but we’ll see a missing template error as our application doesn’t know how to render that action as a PDF.
It looks like we’ve fixed the problem but the format isn’t optional so when we go back to the default view for that page we’ll get a routing error. Fortunately it’s easy to make part of a route optional: all we need to do is wrap the optional part in parentheses, like this:
match "/about(.:format)" => "info#about", :as => :about
Now we can visit either http://localhost:3000/about or http://localhost:3000/about.pdf without seeing a routing error.
Next we’ll show you how to use more complex optional parameters. Let’s say that our application has a number of blog articles that we want to filter by specifying a year with an optional month (or month and day) in the URL.
We can do this by defining a route like this, directing any matching route to our info#about
action. Note that we can nest parentheses when creating optional parameters.
match "/:year(/:month(/:day))" => "info#about"
In the view code we’ll add some debug code so that we can see what parameters are being passed in:
/app/views/info/about.html.erb
<h1>Info#about</h1> <p>Find me in app/views/info/about.html.erb</p> <%= debug params %>
Now if we specify a year in the URL it will be passed into the about action and likewise if we specify a year, month and day all three parameters are passed.
This route is fairly generic though and if we were to visit, say, http://localhost:3000/foo/bar this will also be passed through to the about action when we only want parameters that look like a date to be passed. We can do this with constraints.
Constraints
Constraints are available in Rails 2 where they’re known as requirements. We can pass a :constraints
option to our route with a hash of the values and the constraints that each one should match. We’ll constrain the route so that it will only match if the the year parameter is four digits and the month and day parameters are two digits:
match "/:year(/:month(/:day))" => "info#about", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ }
With this constraint in place when we visit the /foo/bar
path again we get a routing error as the path doesn’t match the date constraints.
What we’ve done with constraints so far was possible with Rails 2’s requirements but with constraints in Rails 3 we can do much more. For example we can use a user_agent
parameter to restrict a route to a specific browser, in this case Firefox.
match "/secret" => "info#about", :constraints => { :user_agent => /Firefox/ }
If we visit http:/localhost:3000/secret in Safari, Chrome or Opera we’ll see a routing error but if we visit in Firefox, or set another browser to identify itself as Firefox, then the user agent passed by the browser will match the constraint and we’ll see the page.
We can also add a constraint for something a little more useful, say the host:
match "/secret" => "info#about", :constraints => { :host => /localhost/ }
This constraint means that we can visit http://localhost:3000/secret and see the page but we’ll get a routing error if we try to visit by using the IP address instead ( http://127.0.0.1/secret ) even though the two addresses are equivalent. This can be used to restrict certain routes to a given subdomain. This ability is currently a little clunky but in a future version of Rails we’ll be able to pass a :subdomain
option to perform this kind of constraint.
If we have a number of routes that match a certain constraint there can be a lot of duplication in the routes file. We can reduce this duplication by using the constraints method and putting the matching routes into a block.
constraints :host => /localhost/ do match "/secret" => "info#about" match "/topsecret" => "info#about" end
There’s much more that can be done with the constraint option but we won’t cover it here.
Routing with Rack
There’s a lot more to routing in Rails 3 than we’ve had time to cover here, but we’ll be coming back to the topic of routing in future episodes. One last feature we’ll take a look at is how Rails 3 routing embraces Rack. Normally we pass a controller and action name to a route but we can also pass in a Rack application which is an extremely powerful feature. To demonstrate this we’ll create a route that points to a simple Rack application.
match "/hello" => proc { |env| [200, {}, "Hello Rack!"] }
If you aren’t familiar with Rack then either watch or read episode 151. All we’re doing above is passing a return code, an (empty) hash of headers and a simple body.
If we visit /hello
in the browser now we’ll see “Hello Rack!” so that we know our Rack application is working and generating the response. This opens up a lot of power and flexibility in how you define routes and route them to different applications. It’s easy now to route to a Sinatra application for example.
Rails 3’s new routing features provide a number of exciting possibilities. Although we’ve only given an overview of the potential here we’ll be looking at certain parts of it in more detail in the future.
For more documentation there is more information about Rails 3 routing on Yehuda Katz’s blog and on RailsGuides.