homeASCIIcasts

203: Routing in Rails 3 

(view original Railscast)

Other translations: Cn Es It

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.

The about action.

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.

The PDF route is now matched.

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.

The date paramters are passed to the route.

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.

The route now doesn't match unless the parameters match a date.

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.

The route only matches when viewed in Firefox

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.

The response is seen from the Rack app.

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.