Blog

The Lowdown on Routes in Rails 3

By | March 30th, 2010 at 11:03AM
Today’s blog post is by a guest community contributor, Rizwan Reza. Rizwan is an active member of the Rails community, and as of late, has been working to clean up the overgrown Lighthouse queue.

Stop! I’d like to tell you something important, but it may be a bit shocking, so you should probably have a seat. Here goes: everything you knew about working with routes in Rails 2… is history! With Rails 3, you’ve got to roll up your sleeves, unlearn what you learned, and route the new way around. And this time, it’s faster, cleaner and a lot more Ruby-like.

In this post, we’ll walk through the underpinnings of Routes in Rails 3. They’ve been rewritten—for good reason—and after we get through the explanation, I’m confident you’ll agree.

Let’s start by looking at some code; here’s the new DSL, in its full glory:

resources :products do
  resource :category
 
  member do
    post :short
  end
 
  collection do
    get :long
  end
end
 
match "/posts/github" => redirect("http://github.com/rails.atom")

Now check out the old way of doing it:

map.resources :products, :member => {:short => :post}, :collection => {:long => :get} do |products|
  products.resource :category
end

As you can see, the example from Rails 3 is much cleaner and more Rubyish. So let’s jump right in and walk through a quick overview of how you’d define different types of routes in Rails 3.

Default Route

The default route in Rails 3, match '/:controller(/:action(/:id))', is much more explicit, as the parenthesis denote optional parameters.

Regular Routes

Rather than defining different keys for controller and action, you just have catalog#view, which is pretty awesome.

match 'products/:id', :to => 'catalog#view'

In Rails 2, you would’ve done:

map.connect 'products/:id', :controller => 'products', :action => 'view'

Named Routes

Named Routes generate helpers like posts_url and posts_path, rather than manually defining the hash to action and controller in helper methods like link_to:

match 'logout', :to => 'sessions#destroy', :as => "logout"

The key :as specifies a name to generate helpers. In Rails 2, you would have done:

map.logout '/logout', :controller => 'sessions', :action => 'destroy'

Empty Route

The root of the web site is the empty route. Whereas Rails 2 added a nice shortcut to it, Rails 3 simplifies things even further:

# Rails 3
root :to => 'welcome#show'
 
# Rails 2
map.root :controller => "welcome", :action => 'show'

Shorthands

The revamped routes in Rails 3 sport some nice shortcuts to commonly used routes. There are two types of shorthands. First, the :to shorthand allows you to skip the :to key and directly designate the route to the matcher:

match "/account" => "account#index"
match "/info" => "projects#info", :as => "info"

Second, the match shorthand allows you to define a path and controller with its action at the same time:

match "account/overview"
 
# identical to
 
match "account/overview", :to => "account#overview"

Verb Routes

While you can limit a route to an HTTP request through :via, it’s a nice added convenience to have Verb routes. Adding sugar on top, you can even use shorthands with them:

get "account/overview"
 
# identical to
 
match "account/overview", :to => "account#overview", :via => "get"

Keys

The match method (as well as the verb shorthands) take a number of optional keys.

:as

The :as key names the route. You can then use named route helpers wherever url_for is available (such as controllers, tests, and mailers). Resource routes (using the resources helper) automatically create named routes, as in Rails 2.3.

match "account/overview/:id", :as => "overview"
 
# in your controller
 
overview_path(12) #=> "/account/overview/12"

:via

Allows you to specify a set of verbs, so only those HTTP requests are accepted for a route.

match "account/setup", :via => [:get, :post]

Rack

Rack is a sweet interface to web servers that provides unified API to Ruby frameworks. Most if not all Ruby frameworks are built on top of Rack these days. The recent built-in support for Rack means your application is not bound to being Rails specific. You can have parts of your application handled by any Rack supported framework, be it Sinatra, Cramp or something else. You can skip the Rails stack altogether and pass on the request to a Rack app.

Here’s an example of a Sinatra app:

class HomeApp < Sinatra::Base
  get "/" do
    "Hello World!"
  end
end
 
Rizwan::Application.routes do
  match "/home", :to => HomeApp
end

And here’s an example of a Rack app:

match "/foo", :to => proc {|env| [200, {}, ["Hello world"]] }
 
match 'rocketeer.js' => ::TestRoutingMapper::RocketeerApp
 
RocketeerApp = lambda { |env|
  [200, {"Content-Type" => "text/html"}, ["javascripts"]]
}

Resourceful Routes

Since Rails 1.2, resourceful routes have been the preferred way to use the router. Recognizing this fact, the Rails core team has added some nice improvements. Take a look at this typical RESTful route in Rails 3:

resources :products

This would generate all the neat helpers we have come to love and would also route the URLs accordingly. Just like before, you can also add multiple resources in a single line:

resources :products, :posts, :categories

More RESTful Actions

As you know, you’re not limited to the seven actions that RESTful architecture provides, but can also define more actions in a resource. Having said that, you might want to keep an eye open if you’re defining lots of actions in a single resource, as they can be turned into separate resources.

We can add RESTful actions to this resource in a couple of ways. Here’s a few collection RESTful actions inside a block:

resources :products do
  collection do
    get  :sold
    post :on_offer
  end
end

And take a look at this inline member RESTful action:

resources :products do
  get :sold, :on => :member
end

Not only that, but you can also redefine to extend the scope of the default seven RESTful actions:

resources :session do
  collection do
    get :create
  end
end

create actions, which usually only accepts POST requests, can now accept GET requests as well:

resource :session do
  get :create
end

Nested Resources

In Rails 2, nested resources were defined by a block or by using a :has_many or :has_one key. Both of these have been superseded by a block, giving them a more Rubyish interface to defining associated resources.

Here’s a route for a project that has many tasks and people:

resources :projects do
  resources :tasks, :people
end

Namespaced Resources

These are especially useful when defining resources in a folder; it doesn’t get much cleaner than this:

namespace :admin do
  resources :projects
end

Renaming Resources

You can also rename resources through the :as key. This code uses :as in resourceful routes to change the products path to devices:

namespace :forum do
  resources :products, :as => 'devices' do
    resources :questions
  end
end

Restricting Resources

Resources can be restricted to only specified actions.

resources :posts, :except => [:index]
 
resources :posts, :only => [:new, :create]

Altering Path Names

You can define a different path name for a particular REST action. This helps you customize your RESTful routes. This code will route /projects/1/cambiar to the edit action.

resources :projects, :path_names => { :edit => 'cambiar' }

The Redirect Method

The newly added redirect method in Rails 3 provides a level of convenience not present before. For example, it can redirect to any path given and eventually, can also pass on to a full-blown URI, something previously accomplished by Rails plugins like redirect_routing.

Moreover, the redirect method also introduces generic actions. Unique to Rails 3, generic actions are a simple way to provide the same action to complex paths, depending on what’s passed to redirect.

This code will redirect /foo/1 to /bar/1s:

match "/foo/:id", :to => redirect("/bar/%{id}s")

This code will redirect /account/proc/john to /johns.

match 'account/proc/:name', :to => redirect {|params| "/#{params[:name].pluralize}" }

Note that redirect cannot be used in a block as opposed to other constraints and scopes.

The Constraints Method

Constraints allow you to specify requirements for path segments in routes. Besides that, they also allow you to use a handful of methods to verify whether or not something matches a given criteria. Like a route that checks if the request is AJAX or not, for example.

In this code, we’re using a regular expression, and the route has been restricted to only allow one digit IDs to pass through:

match "/posts/show/:id", :to => "posts#index", :constraints => {:id => /\d/}

Scope

When the scope method is passed along a symbol, it assumes the symbol is a controller. When the argument is a string, the scope method prepends that string to the beginning of the path.

Scope can also have path_segments, which can be constrained, giving us greater flexibility in routes.

controller :articles do
  scope '/articles', :name_prefix => 'article' do
    scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do
      match '/:id', :to => :with_id
    end
  end
end
 
scope :posts, :name_prefix => "posts" do
  match "/:action", :as => "action"
end
 
scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do # See constraint here
  resources :rooms
end

As you can see, when scope is given a string as its argument, it prepends to the path, something that was accomplished through path_prefix in Rails 2. name_prefix is essentially the same as before.

Optional Segments

Unlike all the previous versions of Rails, path segments can now be optional in routes. Optional segments don’t necessarily have to be path segments, which are passed as parameters to the action. The default route is a good example of optional segments in use. Here, both /posts/new and /posts will be redirected to the create action in posts controller, but /posts/edit will not work:

match 'posts(/new)', :to => 'posts#create'

This is an optional path scope that allows to have a prepended path before a resource:

scope '(:locale)', :locale => /en|pl/ do
  resources :descriptions
end

Pervasive Blocks

As evident from the examples, routes in Rails 3 exhibit pervasive blocks for almost all the methods you’d normally want to pass a block to, helping you achieve DRY in routes.rb.

controller :posts do
  match 'export', :to => :new, :as => :export_request
  match '/:action'
end

Right now, most Rails developers wouldn’t use all of these methods, but when the need arises—when you need to define more complex routes—you might appreciate having the above information handy. You don’t need to use a plugin or a hack when you know it’s built right in. With Rails 3, Routes rock at a whole new level.

Questions and comments welcome!

  • Sammy

    Still missing :shallow option on resources.

  • http://intensedebate.com/people/rizwanreza rizwanreza

    Shallow routes haven't been implemented yet. We're working on that here: https://rails.lighthouseapp.com/projects/8994/tic…

    Thanks.

  • Hique

    Also is missing the ability to pass a parameter with a fixed value through the route, like:

    match 'photos/:id' => 'photos#show', :format => 'jpg'

    Unfortunately this doesn't work on Rails 3.0.0.beta, so I am stuck with map.connect. Don't now if it works on edge though.

  • guns

    With regards to `:constraints => {:id => /d/}' limiting :id to _one_ digit, I though you had a typo there until I tried it myself.

    This feel surprising. Does the router surround the :contraints regexps with implicit anchors? Normally, `/^d$/' would match one digit and no more.

  • guns

    backslash-d of course; the comment filter stripped those out

  • Chris

    Looks like Merb. Hmm. Maybe we should call in Merb 2 and not Rails 3. Then we can loose all the negative baggage that comes with the "Rails" image.

  • bryanl

    constraints are even cooler than that

    class SiteAdminDomain

    def self.matches?(request)

    !!Site.by_request # find a site matching the request

    end

    end

    b

    # in config/routes.rb

    constraints(SiteAdminDomain) do

    resources :assets

    end

  • Sammy

    And rename actionpack to rails-core.

  • http://railsjedi.com Jacques Crocker

    One very cool feature is the ability to easily work subdomains into your routes. Might be worth mentioning how in the post.

  • javier ramirez

    If I understood well, where you said "catalog#view" you probably meant "products#view".

    Thanks for the great work!

  • Kunal Shah

    Fantastic write up.

    Does:
    match "/somepath", :to => SomeApp

    essentially replace Rails Metal? Is there still a performance benefit to catching it earlier on?

    Also, not that you can do this easily in 2.3, but in the updated dsl would I easily be able to direct both:

    /posts/the-title-of-a-post
    /posts/5

    to the same action on the same controller in the same route?

  • kamalfariz

    Is there any support for subdomains?

  • bryanl

    constraints are even cooler than that….

    cont

  • http://intensedebate.com/people/rizwanreza rizwanreza

    No, the router doesn't surround Regular Expressions with implicit anchors right now.

  • http://intensedebate.com/people/rizwanreza rizwanreza

    I wasn't able to work through a working example of subdomains yet. I think the new routing DSL is flexible enough to be extended for such purposes. Or may be, I am wrong that subdomains aren't in yet.

  • http://intensedebate.com/people/rizwanreza rizwanreza

    See this: http://github.com/rails/rails/commit/fef7a77386b0…

    I will work through an example and add it to the article. Thanks.

  • http://www.layer22.com Piotr Usewicz

    It would be great if you guys updated the article with actual results.

  • Piotr Usewicz

    It's easier to see the generated paths than just looking at the code.

  • http://www.stefanobernardi.com Stefano Bernardi

    As a noob, I would easily agree with this!

  • Scott Johnson

    I think you should have more explanation of exactly what routes are created by some of these. For example the "Renaming Resources" it's not clear to me whether the path is /products and the controller is 'devices', or vice versa.

    If you could show some example paths created, and what controller/action they map to, that would be a big help.

  • Cyrille

    what about I18n ? Shall we still need translate_route plugin with rails 3 ?

  • http://intensedebate.com/people/rizwanreza rizwanreza

    I agree, we'll see what we can do about that.

  • http://intensedebate.com/people/rizwanreza rizwanreza

    From the looks of it, it will still require a plugin to support such functionality.

  • Shilo

    How about passing custom parameters to the controller? Say I want to render the same action with different data, depending on the url. in Rails 2 it was done as

    map.products '/products', :controller => :products, :action => :show, :type => 'simple'
    map.assets '/assets', :controller => :products, :action => :show, :type => 'complex'

    Then the type would be available under params[:type]. I could then use it for something like

    @products = Product.by_type(params[:type]).all # using some named_scope

    Any way to do this in R3?

  • sjsadowski

    Oh, great breakdown – thanks Rizwan!

  • http://intensedebate.com/people/rizwanreza rizwanreza

    Well, that can be done easily with new Routes:

    match /products/show(/:type)' => 'products#show' # Available as params[:type]
    match /assets/show(/:type)' => 'products#show'

    If you'd rather not allow the user to mention the type in the URL:

    match /products/show' => 'products#show', :type => 'simple' # Available as params[:type]
    match /assets/show' => 'products#show', :type => 'complex'

  • http://intensedebate.com/people/anywho anywho

    after reading this, it seems it's actually safer to just use
    get, post, put, and delete as opposed to match. thoughts?

  • Joost Baaij

    There are a couple of typos in this post, but the other commenters have pointed those out already.

    While I like the new routing DSL a lot, I disagree it's more "Rubyish". What is Rubyish anyway? The old DSL is Rubyish, just as the new one. It's just different. Your example in one long line doesn't make it any less Rubyish. The new way of doing things is just different, and uses a different coding style.

  • Dan

    this is how I've translated my routes

    #config/routes.rb
    match I18n.t('string.forbidden') => 'page#forbidden', as: :forbidden
    resources :companies, as: I18.t('string.companies')

    #config/locales/sv.yml
    sv:
    string:
    forbidden: "förbjuden"
    companies: "företag"

    It's important that you use I18n when translating your routes. If you type non usascii text in the routes file you will get an encoding error. You must also unescape your urls before rails tries to match the urls with your routes. Apache or Nginx can help you with this, alternatively you can path rails to unescape the url for you.

  • jack

    The "redirect cannot be used in a block" confused me for a sec…I read that as "with a block" and thought, man that's too bad…until I looked at the code.

    FWIW it's definitely possible to pass a block to redirect and have a really nice and super fast redirect take place:

    match "/articles/:id" => redirect { |params| a = Article.find(params[:id]); a.url }

  • http://www.jarijokinen.com/ Jari Jokinen

    Does this really work?

    First problem in Rails 3 is that locales are loaded after routes.This means, if you start the app, you get "translation missing" errors. This problem can be solved by calling Foobar::Application.reload_routes! at the end of the config/environment.rb file.

    Also, where do you set your locale (I18n.locale = "sv")?

    I'm storing the locale value in session[:locale] and set it in the ApplicationController#set_locale using before_filter, but of course this doesn't work with routes because I think routes are application specific and not session specific?

    Or am I just doing something wrong?

  • http://intensedebate.com/people/rizwanreza rizwanreza

    My point was in regards to the pervasive blocks that the new DSL promotes.

    Can you point out the typos? Thanks!

  • http://intensedebate.com/people/rizwanreza rizwanreza

    You're right, my bad! :)

    I meant this:

    redirect("http://github.com/rails.atom&quot;) do
    # routes here
    end

    This is not supported, last I checked. Thanks!

  • http://intensedebate.com/people/rizwanreza rizwanreza

    You are right but it still depends on the scenario.

  • Paul

    Thanks for the great article!

    Is there a way of specifying a subdirectory, like the way relative_url_root works? If not, what is the recommended way of doing that in Rails 3?

    (And I could follow this article better if the physical URL were put under each example.)

  • http://intensedebate.com/people/rizwanreza rizwanreza

    After setting up your redirect rules either in a middleware or in Apache, you can use scope blog as shown above and write all routes inside the block.

  • Kristian Mandrup

    I wanted to upgrade a previous i18n plugin for Rails to translate the path. In Rails 2, there were path segments of the kinds seen in this articlehttp://railsguts.com/routing_inside_out.html, such as StaticSegment.
    I couldn't find anything similar in Rails 3. How would I go about translating the static parts of the path in Rails 3?

    Fx localhost:3000/home –> localhost:3000/hogar (spanish)

    Thanks!

  • http://ksylvest.com/ Kevin Sylvestre

    Thanks for the great article! Just one question: Is it possible to match a route composed of only optional parameters?

    For example:

    match '(/name/:name)(/weight/:weight)(/height/:height)' => 'people#index', :as => :people

    Matching:

    /
    /name/kevin
    /name/kevin/weight/80
    /weight/80/height/180

  • http://blog.yakitara.com/ hiroshi

    If you can't get the solution of getting rid of

    "DEPRECATION WARNING: You are using the old router DSL which will be removed in Rails 3.1. Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-o…

    Just try:

    – <%= app_const %>.routes.draw do |map|
    + <%= app_const %>.routes.draw do

  • Jérémy Lecour

    Sorry, I've wrote a little blog post about this (http://wp.me/p6W-4H) before I find your comment. It was not intentional.

  • http://seanbehan.com sean behan

    in rails 2.* it was possible to do this
    map.resources :users, :as => 'members'
    which will map /members path to users_controller.rb with "users_path" helper and "form_for(@user)" helper all working together

    rails 3.* breaks this and i have not read in the docs how to accomplish the same thing to rename your routes. doing "resources :users, :as => 'members'"
    would require "members_path" as a helper but passing the @user object to "form_for" expects the "users_path" …

    thoughts?

  • Brian Maltzan

    why do this:

    root :to => 'content#index'

    instead of this:

    root to: 'content#index'

    thanks!

  • http://snowhitesolutions.com Tim Snowhite

    because to: is not a valid symbol in ruby.
    One can't define parameters seperated by ':' in ruby as one can in javascript or Objective-C.

  • Miro Zahorak

    Thanks for nice summary Brian,
    does anybody have any idea how to use this in rails 3:

    Translate::Routes.translation_ui(map) if RAILS_ENV != "production"

    it is used by translate plugin, which i am trying to get to work under rails 3 https://github.com/newsdesk/translate

    Thanks in advance

  • Miro Zahorak

    Thanks for nice summary Brian,
    does anybody have any idea how to use this in rails 3:

    Translate::Routes.translation_ui(map) if RAILS_ENV != "production"

    it is used by translate plugin, which i am trying to get to work under rails 3 https://github.com/newsdesk/translate

    Thanks in advance

  • Alex

    Any progress on shallow routes??

  • http://cinemasetfree.com Law

    here's a question.
    why has rails 3 no implementation for cross subdomain routing?
    for example:
    get '/login' => "devise/sessions#new", :as => :login, :subdomain => "secure"
    # => secure.mydomain.com/login

    how does one define a route such as that?

  • http://benjamincoppock.com Ben

    I'm having trouble finding documentation on routing for SSL connections using Rails 3.

    Specifically, I'm wondering how to force SSL, but only given a condition (such as a user being logged in). Is this possible?

  • tompata

    You can also group restful actions within member and collection, for example:
    resources :users do
    collection do
    get :update_select, :update_value
    end
    end

  • Kostas K.

    with actionpack 3.0.3 (rails 3.0.3) you can do:
    match 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'}

    and it will have the same result

  • Anonymous

    Nice tutorial, I saved a lot of time. Thx

  • http://www.facebook.com/people/Said-Kaldubaev/100001506759434 Said Kaldubaev

    very nice :)

  • http://twitter.com/l___i Aji

    kollaam

  • http://www.facebook.com/vshakirov Vladimir Shakirov

    Hi
    I do like this but it doesn’t work

    resources :products do
    resource :category

    member do
    post :short
    end

    collection do
    get :long
    end
    end

    Callback
    undefined local variable or method `long_product_category_path’ for #<#:0x1036e92a0>

    Could you help

  • http://twitter.com/tetu1225 Fujiki Taiju

    It’s very nice!

  • http://profiles.google.com/ben.turner Ben Turner

    I picked up Rails brand new three weeks ago, producing a very non-REST web app, and feel like I’ve been swimming against the current trying to get my routing working.

    Finally, this post has “clicked ” few more things, things which might have already been obvious had I come here via Rails1 or Rails2. Thanks man, nicely laid out, and I think I’m ready to rejig my routes file a bit cleaner !

  • Arivarasan

    good work! 

    Thanks for the post
    but i have a issue,

    in my routes.rb
    namespace :manage do root :to => “index#index” resources :listings do get :choose, :on => :member # member route end end

    in my view

    undefined local variable or method `choose_manage_listings’_path for #<#:0x33cafc8>

    Help me!

    Thanks,
    L. Arivarasan

  • Anonymous

    I read somewhere that 3.1 was doing rescue_from routing errors differently — can someone confirm and/or point me to some documentation/tutorials?  I’m not finding anything (including the site where I *think* I read that……

  • http://www.blog.roshanbook.com/2011/11/23/ruby-on-rails-resources/ Ruby on Rails resources | Roshan Book

    [...] The Lowdown on Routes in Rails 3 – Rizwan Reza gives us a great summary of all the different ways to create routes in Rails 3. [...]

  • Mansi Singhal

    Thanks for the post !!!

  • Anonymous

    Thank you, thank you, thank you, thank you, thank you.  The part on :via really solved a problem i’ve been trying to solve for days.