Rails routing can shock developers who are used to putting their code in files wherever they want to put them. After the directory-based approach of traditional HTML and template-based development, Rails’ highly structured approach looks very strange. Almost nothing, except for a few pieces in the public folder, is anywhere near where its URI might have suggested it was. Of course, this may not be so shocking if you’ve spent a lot of time with other frameworks or blogs—there are many applications that control the meanings of URIs through mechanisms other than the file system.
If you prefer to read “URI” as the older and more familiar URL,
that’s fine. Everything works the same here. (And the core method Rails
uses to generate URIs is, of course, url_for
, in the UrlModule
.)
Rails routing turns requests to particular URIs into calls to particular controllers and lets you create URIs from within your applications. Its default routing behavior, especially when combined with resource routes generated through scaffolding, is often enough to get you started building an application, but there’s a lot more potential if you’re willing to explore Rails routing more directly. You can create interfaces with memorable (and easily bookmarkable) addresses, arrange related application functionality into clearly identified groups, and much, much more.
What’s more, you can even change routes without breaking your application’s user interface, as the routing functionality also generates the addresses that the Rails view helper methods put into your pages.
Changing routing can have a dramatic impact on the web services aspect of your applications. Programs that use your applications for XML-based services aren’t likely to check the human interface to get the new address, and won’t know where to go if you change routing. Routing is effectively where you describe the API for your projects, and you shouldn’t change that too frequently without reason.
Rails routing is managed through a single file, config/routes.rb. When Rails starts up, it loads this file, using it to process all incoming requests.
If you’re in development mode, which you usually are until deployment, Rails will reload routes.rb whenever you change it. In production mode, you have to stop and restart the server.
The default routes.rb contains a lot of help information that can get you started with the routes for your application, but it helps to know the general scheme first. In routing, Rails takes its fondness for connecting objects through naming conventions and lets you specify the conventions. Doing that means learning another set of conventions, of course!
The easiest place to start is with the default rules, as you’ve probably already written code using them without having examined them too closely. They are near the bottom of the file and get called if nothing above them matched. You’ll always want to put higher-priority routes above lower-priority ones, since the first match wins. The default rules look like:
map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format'
The map.connect
method is the
foundation of routing, though what it does can be mysterious until you
compare it to some actual URIs. (Again, for the purposes of this
chapter, if you’d rather read URI as URL, that’s fine.) For example,
if your Rails server on localhost, port 3000, had a controller named
people
that had an action named
show
, and you wanted to apply that
to the record with the id
of 20,
this rule would let you do that with a call to:
http://localhost:3000/people/show/20
When Rails gets this, it looks for a rule that looks like it
matches the URI structure. It checks the default rules last, but when
it encounters the first default rule, Rails knows from this to set
:controller
to people, :action
to show, and :id
to 20. The symbols (prefaced with
colons) act as matching wildcards for the routing. Rails uses that
information to call the PeopleController
’s show method, passing it
20 as the :id
parameter.
If a request comes in that would match ':controller'
or ':controller/'
, Rails assumes that the next
piece would be index
, much as web
servers expect index.html to be a
default file. The :action
will be
set to index
. Also, Rails ignores
the name of the web server in routing, focusing on the parts of the
URI after the web server name.
Routing rules also work in reverse. The link_to
helper method and the many other
methods link_to
supports can take a
:controller
, an :action
, and optionally even an :id
, and generate a link to a URL for
accessing them. For example:
link_to :controller => 'people', :action => 'show', :id => 20
would, working with the default rules, produce:
http://localhost:3000/people/show/20
The second rule is much like the first, with one piece of extra functionality. If a user wanted to request XML specifically, she could write:
http://localhost:3000/people/show/20.xml
Because these match the second map.connect
rule, Rails will set :controller
to people
, :action
to show
, :id
to 20, and :format
to xml
. Then it uses that information to call
the PeopleController
’s show
method, passing it 20
as the :id
parameter and xml
as the :format
parameter.
If your controller checks the :format
parameter—Chapter 5 examined
respond_to
, the easiest way to do
this—and the value is one you’ve checked for, your controller can send
a response in the requested format. This isn’t limited to HTML or
XML—you can specify other formats through the extension. If your
controller supports them, visitors will get what they expect. If not,
they might be disappointed, but nothing should break.
You could and probably should also specify the format through the MIME content-type header in the HTTP request, but that doesn’t get checked in ordinary Rails routing.
There are many different ways to use map.connect
. The approach that the default
rules take—presenting a string filled with symbols that connect to
pieces of a URI—is simple, but a rather blunt instrument. The map.connect
method offers another approach
that lets you specify URIs quite precisely: explicit specification of
the URI and directions for where to send its processing. This looks
like:
map.connect 'this/uri/exactly', :controller => "myController", :action => "myAction"
Using this rule, if a request comes in to a Rails server at localhost:3000, looking like:
http://localhost:3000/this/uri/exactly
Rails will call the myController
controller’s myAction
method to handle the
request.
In many older Rails applications and in documentation, you’ll
often see map.connect
used with a
blank string as its first argument, as in:
map.connect '', :controller => "somewhere", :action => "something"
This tells Rails what to do if the URI points at the top level
of the server. It still works, but using map.root
, covered later in this chapter,
is now considered better practice. (You also need to delete
index.html
from the public directory.)
While explicitly declaring mappings from individual URIs to particular controller actions is certainly precise, it’s also not very flexible. Fortunately, you can mix symbols into the strings however you think appropriate to create combinations that meet your needs. For example, you might have a route that looks like:
map.connect ':action/awards',
:controller => 'prizes'
if Rails encountered a URI like:
http://localhost:3000/show/awards
Then it would route the call to the show
method of the prizes
controller.
The map.connect
method
supports one other important technique. Calls to it aren’t limited to
the :controller
, :action
, :id
, and :format
parameters. You can call it with any
parameters you want—:part_number
,
:ingredient
, or :century
, for example—and those parameters
will be sent along to the controller as well. What’s more, you can mix
those symbols into the URI string for automatic extraction, making it
possible to create routes like:
map.connect 'awards/:first_name/:last_name/:year', :controller => 'prizes', action => 'show'
Then it would route the call to the show
method of the prizes
controller, with the arguments
:first_name
, :last_name
, and :year
.
Often when prototyping, developers (and especially designers) like to start with the top page in a site, the landing page visitors will see if they just enter the domain name. The vision for this “front door” often sets expectations for other pages in the site, and the front door gets plenty of emphasis because it’s often the first (or only) page users see. Even in an age where Google sends users to pages deep inside of a site, users often click to “the top” to figure out where they landed.
There are two ways to build this front door in Rails. The first way, which may do well enough at the outset, is to create a static HTML file that is stored as public/index.html. That page can then have links that move users deeper into your application’s functionality. It’s more likely, however, that projects will quickly outgrow that, as updating a static page in an otherwise dynamic application means extra hassle when things change.
The second approach deletes public/index.html and uses routing to
specify where to send users who visit just the domain name. Before
Rails 2.0, this was done by specifying map.connect
for an empty string, but Rails
2.0 introduced map.root
, a cleaner
way of making the connection. The map.root
method looks just like map.connect
, but doesn’t have the first
method. If you want visitors to the domain name to receive a page from
the entry method of the welcome controller, you could write:
map.root :controller => welcome, :action => entry
You could also specify :id
and any other parameters you want, just as with map.connect
.
Using wildcards makes it likely—even probable—that more than one routing rule applies to an incoming URI. This could have produced an impenetrable tangle, but fortunately Rails’ creators took a simple approach to tie-breaking: rules that come earlier in the routes.rb file have higher priority than rules that appear later. Rails will test a URI until it comes to a match, and then it doesn’t look any further.
In practice, this means that you’ll want to put more specific rules nearer the top of your routes.rb file and rules that use more wildcards further down. That way the more specific rules will always get processed before the wildcards get a chance to apply themselves to the same URI.
While you could use map.connect
for all of your routes, you’d
miss out on a lot of convenience facilities Rails could provide your
application. By naming routes, you gain helper methods for paths and
URLs, making your application more robust and more readable.
How do you name a route? It’s simple—just replace connect
with the name of your route. For
example, to create a route named login
, you could write:
map.login '/sessions/new', :controller => 'sessions', :action => 'new'
Once you’ve done this, you’ll have two new helper methods,
login_path
and
login_url
. The first will return
/sessions/new
and the second
http://localhost:3000/sessions/new
(if you’re running it in the default server). That may not seem that
important, but once you have
something like this scattered through your views:
<%= link_to "Login", login_path %>
it’s nice to be able to change where those point just by modifying a single line of the routes.rb file.
While it’s useful to have the default route retrieve an id
value and pass it to the controller, some
applications need to pull more than one component from a given URI.
For example, in an application that makes use of taxonomies (trees of
formal terms), you might want to support those tree structures in the
URI. If, for example, “floor” could refer to “factory floor” in one
context, “dance floor” in another context, and “price floor” in yet
another context, you might want to have URIs that looks like:
http://localhost:3000/taxonomy/factory/floor http://localhost:3000/taxonomy/dance/floor http://localhost:3000/taxonomy/price/floor
The only piece that the routing tool needs to be able to identify is taxonomy, but the method that gets called also needs the end of the URI as a parameter. A route that can process that might look like:
map.taxonomy 'taxonomy/*steps'
:controller => 'taxonomy', action => 'showTree'
The asterisk before steps
indicates that the rest of the URI is to be “globbed” and passed to
the showTree
method as an array,
accessible through the :specs
parameter. The showTree
method
might then start out looking like:
def showTree steps = params [:steps] .... end
If the method had been called via http://localhost:3000/taxonomy/factory/floor, the
steps
variable would now contain
[ 'factory', 'floor' ]
; if called
via http://localhost:3000/taxonomy/factory/equipment/mixer,
the steps
variable would now
contain [ 'factory',
'equipment', 'mixer' ]
. Globbing makes it possible to gather
a lot of information from a URI.
While Rails is inspecting incoming request addresses, you might want
to have it be a little more specific. For example, you might create a
route that checks to make sure that the id
values are numeric, not random text, and
presents an error page if the id
value has problems. To do this, you can specify regular expressions in
parameters for your routes:
map.connect ':controller/:action/:id', :id => /d+/
map.connect ':controller/:action/:id', :controller => 'errors', :action => 'bad_id'
The first rule looks like the default rules, but checks to make
sure that the :id
value is composed
of digits. (Regular expressions are explained in Appendix C.) If the
id
is composed of digits, the
routing goes on as usual to the appropriate :controller
and :action
with the :id
as a parameter. If it isn’t, Rails
proceeds to the next message, which sends the user to a completely
different errors
controller’s
bad_id
method.
If you’re building REST-based applications, you will become very
familiar with map.resources
. It
both saves you tremendous effort and encourages you to follow a common
and useful pattern across your applications. Chapters 5 and 9 have already explored how REST
works in context, but there are a few more options you should know
about and details to explore. A simple map.resources
call might look like:
map.resources :people
That one line converts into 14 different
mappings from calls to actions. Each REST-based controller has seven
different methods for handling requests, and the routing has to handle
cases with and without a :format
property. Table 15-1
catalogs the many things this call creates.
Table 15-1. Routing created by a single map.resources call
Name | HTTP method | Match string | Parameters |
---|---|---|---|
people | GET | /people | {:action=>"index", :controller=>"people"} |
formatted_people | GET | /people.:format | {:action=>"index", :controller=>"people"} |
POST | /people | {:action=>"create", :controller=>"people"} | |
POST | /people.:format | {:action=>"create", :controller=>"people"} | |
new_person | GET | /people/new | {:action=>"new", :controller=>"people"} |
formatted_new_person | GET | /people/new.:format | {:action=>"new", :controller=>"people"} |
edit_person | GET | /people/:id/edit | {:action=>"edit", :controller=>"people"} |
formatted_edit_person | GET | /people/:id/edit.:format | {:action=>"edit", :controller=>"people"} |
person | GET | /people/:id | {:action=>"show", :controller=>"people"} |
formatted_person | GET | /people/:id.:format | {:action=>"show", :controller=>"people"} |
PUT | /people/:id | {:action=>"update", :controller=>"people"} | |
PUT | /people/:id.:format | {:action=>"update", :controller=>"people"} | |
DELETE | /people/:id | {:action=>"destroy", :controller=>"people"} | |
DELETE | /people/:id.:format | {:action=>"destroy", :controller=>"people"} |
For all of the routes that use HTTP GET methods, Rails creates a
named route. As discussed later in the chapter, you can use these to
support _path
and _url
helper
methods with link_to
and all of the
other methods that need a path or URL for linking.
If your application contains Ruby singleton objects, you should use map.resource
rather than map.resources
for its routing. It does
most of the same work, but supporting a single object rather than a
set. (Singleton objects have an include
Singleton
declaration in their class file, which marks it
as deliberately allowing only one object of that kind in the
application.)
This map.resources
call, its
14 routes, and the supporting seven controller methods are all it
takes to support the scaffolding. However, there will likely be times
when you want to add an extra method to do something specific. You can
do that without disrupting the
existing RESTful methods by using the :member
or :collection
options. :member
lets you specify actions that apply
to individual resources, whereas :collection
lets you specify actions
that apply to a set of resources. For example, to add the roll
method to the courses
resource, Chapter 9 called:
map.resources :courses, :member => { :roll => :get }
In addition to the 14 methods, the routing now supports two
extra. The named routes roll_course
and the formatted_roll_course
both
use the GET method, as the parameter suggests. Both call the roll
method on the courses
controller, which you’ll have to
create. The formatted_roll_course
route adds a formatting parameter if one was provided.
If you need multiple extra methods, you just list them in the
:member
options hash:
map.resources :courses, :member => { :roll => :get, :history => :get, :student_attendance => :get }
Chapter 9 went into extended detail on the many steps necessary to create an application using RESTful nested resources, in which only awards that applied to a given student were visible. Making that change required a shift at many levels, but the change inside of the routing was relatively small. Instead of two routing declarations in routes.rb:
map.resources :awards map.resources :students
there was only one, combining them:
map.resources :students, :has_many => :awards
The resulting routes still create 14 routes for :awards
, but they all look a little
different. Instead of names such as award
and new_award
, they shift to student_award
and
new_student_award
, highlighting their
nested status. Their paths are all prefixed with /student/:student_id
,
as the award-specific parts of their URIs will appear after that,
“below” students in the URI hierarchy.
The declaration map.resources
:students, :has_many => :awards
is actually an abbreviated form, short
for the more verbose:
do |students| students.resources :awards end
If you need to add extra methods to the :awards
resource, you’ll need to use this
longer form, as shown in Chapter 17.
You can also specify multiple resources to nest by giving
:has_many
an array as its argument. If students also have, say,
pets, you could make that a nested resource as well in a single
declaration:
map.resources :students, :has_many => [:awards, :pets]
As your list of routes grows, and especially as you get
into some of the more complicated routing approaches, you may want to
ask Rails exactly what it thinks the current routes are. The simplest
way to do this is to use the rake
routes
command. Sometimes its results won’t be a big
surprise, as when you run it on a new application with only the
default routes:
/:controller/:action/:id /:controller/:action/:id.:format
If you run it on a more complicated application, one with resources, you’ll get back a lot more detail—names of routes, methods, match strings, and parameters:
students GET /students {:action=>"index", :controller=>"students"} formatted_students GET /students.:format {:action=>"index", :controller=>"students"} POST /students {:action=>"create", :controller=>"students"} POST /students.:format {:action=>"create", :controller=>"students"} new_student GET /students/new {:action=>"new", :controller=>"students"} formatted_new_student GET /students/new.:format {:action=>"new", :controller=>"students"} edit_student GET /students/:id/edit {:action=>"edit", :controller=>"students"} formatted_edit_student GET /students/:id/edit.:format {:action=>"edit", :controller=>"students"} student GET /students/:id {:action=>"show", :controller=>"students"} formatted_student GET /students/:id.:format {:action=>"show", :controller=>"students"} PUT /students/:id {:action=>"update", :controller=>"students"} PUT /students/:id.:format {:action=>"update", :controller=>"students"} DELETE /students/:id {:action=>"destroy", :controller=>"students"} DELETE /students/:id.:format {:action=>"destroy", :controller=>"students"} ...
And that’s just for one resource! Note that Rails lines these routes up on the HTTP method being called, which is not always the easiest way to read it. If you have lots of routes, and especially lots of resources, you’ll need some good search facilities to find what you’re looking for.