The Lowdown on Routes in Rails 3
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. We’re happy to have him write for those seeking advice on deploying on a Platform as a Service.
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 (and those doing Rails deployment with Engine Yard) 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 and Rails hosting on Engine Yard, Routes rock at a whole new level.
Questions and comments welcome!
Share your thoughts with @engineyard on Twitter