Routes
The Rails Router
The router has, essentially, a simple job. Many Rails programmers, though, consider it a scary place. There’s no reason it should be complicated.
The router is controlled through the config/routes.rb
file. This is one of the first files to open when evaluating someone else’s project. Ask questions like these:
- Is it organized, or is it just a junk drawer? Organized is good, messy is bad.
- Are they using a REST style or an old-school RPC style? REST is good, RPC is bad.
- If REST…
- Are they using many custom actions? A couple custom actions are ok, but if there are many they probably don’t understand REST.
- Are they using nested resources?
- Do they map convenience routes (like
/login
and/logout
)? If yes, then they’ve put some energy into the routes, a good sign.
REST
REpresentational State Transfer, or REST, is a pattern described by Roy Fielding in 2000. It describes a simple model of interacting with "resources" on the web. More information can be found on Wikipedia: http://en.wikipedia.org/wiki/Representational_State_Transfer
REST in Rails
Rails 2 was the first framework to bring REST into the mainstream. Now with Rails 3 REST is the de facto standard, using the old RPC approach is passé.
The Rails implementation of the REST pattern defines five essential actions:
- Listing resources accomplished with
index
- Displaying a single resource accomplished with
show
- Deleting a single resource accomplished with
destroy
- Creating a new resource accomplished with
new
andcreate
- Changing a single resource accomplished with
edit
andupdate
Each action is triggered by a unique combination of the request verb and the path.
Request Verb
The HTTP protocol defines the verbs GET
, PUT
, POST
, and DELETE
. The concept of the protocol is that all resources on the web can be manipulated with these verbs.
The trouble is that browsers don’t actually implement all four verbs. Also, until recently, HTML forms only supported GET
and POST
. This leads to some complication.
Rails circumvents these limitations by faking the request verbs using a _method
parameter. If a POST
request comes in with no _method
, the router will consider it a POST
. If a POST
comes in with the parameter _method
set to "delete"
, however, the router will consider it a DELETE
.
When we look at the routing table we pretend that our server will be handling all four verbs, while in the back of our mind knowing that there’s some magic going on.
Request Path
The second component the router needs to select the correct action is the request path. Typically these are in one of these forms:
/resources/
when referring to the collection/resources/id
when referring to a single element/resources/id/edit
to trigger the edit action, not really a part of REST
RESTful routes combine these paths and verbs in the routing table.
Controlling the Router
As mentioned above, we control the router through the config/routes.rb
file. The syntax of this file has changed several times with different versions of Rails which is one reason developers get tripped up – there is a lot of old documentation out there.
Let’s look at the essential techniques.
The Routing Table
To understand the router’s configuration, the best tool is the routing table. From within a project root, you can display the routing table by running rake routes
like this:
Terminal
$
|
|
The table is blank! Obviously this means no routes have yet been defined.
Standard REST
Standard RESTful routes are added to config/routes.rb
like this:
1 2 3 |
|
Now, with that one line added, when I run rake routes
I see this:
Terminal
$ |
|
Declaring that I have resources called articles and following Rails’ RESTful pattern adds seven entries to the routing table.
Routing Table Entries
An entry in the routes table looks like this:
1
|
|
What do those components pieces mean?
- Column 1, here
articles
: This is the "name" of the route. In our application we can use it to generate URLs. There are helpers for(name)_url
and(name)_path
. The former generates a full (absolute) URL including the protocol and server (likehttp://localhost:3000/articles/
). The latter creates a URL relative to the site root (like/articles/
). The(name)_path
method (e.g.articles_path
) is preferred. - Column 2, here
GET
: The request verb that must be matched to trigger this route - Column 3, here
/articles(.:format)
: The request path that must be matched to trigger this route. We’ll deal with parameters like:format
in greater detail later. - Column 4, here
{action:"index", controller:"articles"}
: Which controller and action (or method in the given controller class) will be triggered when the route is matched
If that make sense, you might be confused by the following line in the routes table:
1
|
|
Where’s the name in column 1? The way the table is formatted is for the names to "inherit down". Since this line has no listed name, it inherits the name from the line above it, here articles
, or for practical purposes articles_path
. Since the name and path are identical for multiple routes, Rails uses the request verb to distinguish between them.
Handling Parameters and Formats
The only other complex part about a table entry is the path. Here are the unique path patterns from the above table:
1 2 3 4 |
|
You can think of these patterns as very simplistic regular expressions. When you see a colon then a string of letters, such as :id
or :format
, this is a marker which names the data in that position.
Looking at the last pattern in that list (/articles/:id(.:format)
), it would match a request for /articles/16
and store 16
into the parameter named :id
. Within our controller we would access this particular parameter with params[:id]
.
That last pattern would also match a request for /articles/16.xml
, storing 16
into the :id
(i.e. params[:id]
) and xml
into the parameter :format
(i.e. params[:format]
). In the pattern, the parentheses around .:format
tell the router that this part is optional.
In .:format
, the .
is a literal period character. So /articles/16.xml
will match, but /articles/16xml
will not work properly.
Note: Though the :id
is normally a numeric ID corresponding to the unique key in the database, it doesn’t have to be. You can build your app to handle other slugs to lookup resources. The router will just blindly put whatever part of the url is in the :id
spot into the params[:id]
, then it’s up to you (and your controller) to use it correctly.
Custom Member Actions
The REST pattern is very constraining, and that’s a good thing. When developers first start with REST they want to add custom actions for just about everything. Resist this temptation!
The O’Reilly book RESTful Web Services does an excellent job of explaining how to design resources to follow the REST pattern. If you’re struggling with RESTful design, read this book!
It is my opinion that anything we add in a custom action should be available using a standard REST action. For instance, a typical example for a content management system would be the act of publishing. Assume that we have resources articles
and at the data layer we’re storing a boolean value named published
.
This data value should be accessible in the form used for both the create
and edit
actions. But, for convenience, we want to add a “PUBLISH!” button to our index
page. That way our administrators could easily publish articles from the index
without going into the edit
form.
This kind of augmentation is a great use of a custom route. It’s not replacing edit
or doing something that should be handled in another resource, it’s adding an easy way to access something that’s already there.
A member action will work on just a single resource, a single article, as opposed to the collection of all articles. We’d modify our routes.rb
like this:
1 2 3 4 5 6 7 |
|
We’re passing a block into the resources
method. That block calls the member
method and pass it another block. That block calls the put
method with the string publish
. The effect is that we build a route to recognize a PUT
verb to a member path.
Why use the PUT
verb? If you’re going to change data you should use a PUT
or a POST
. Since this publish
action is creating a simplification of the edit
/update
, it makes sense to use the same verb that would have been used by update
– PUT
.
Run rake routes
to see the details and you’d get this entry:
1
|
|
This looks just like the path for the update
action except for the /publish
on the end of the pattern. When this entry is matched the router will trigger the publish
action in ArticlesController
. To reference this path, we’d use the helper publish_article_path
which needs the ID number as a parameter (e.g. publish_article_path(16)
).
Custom Collection Routes
Even more rare are custom actions on a collection of resources. Let’s say, for the sake of example, that we decided to create a publish_all
action.
We’d start by modifying the routes.rb
like so:
1 2 3 4 5 6 7 8 9 10 11 |
|
We call the collection
method which takes a block, and in that block again call the put
method and give it the string publish_all
. Run rake routes
and we’d see a new route like this:
1
|
|
It can be used by calling the publish_all_articles_path
helper with no parameter and would trigger the publish_all
action in ArticlesController
.
Nested Resources
Nested resources sound like a great idea because they can build up beautiful URLs. For instance, let’s say our articles are going to have comments. For an article with ID 16
we might want to list the comments with this URL:
1
|
|
To create this, we nest the comments
resource inside the articles
like below:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Then run rake routes
and you’ll see seven new routes show up:
Terminal
|
Going back to our example, we could now call article_comments_path(16)
to generate the URL /articles/16/comments
. It works!
That being said, every time I use nested resources I regret it. I almost always end up ripping them out later.
Imagine we record the user who posts the comment. Then you want to browse all comments by a certain user with ID 15
across articles. What URL would you go to? You’ll end up building /comments?user=15
, a normal un-nested resource. Now you’ve got both the nested version and the un-nested version, sets of helpers for article_comments_path
and comments_path
, and things get confusing quickly.
Instead, knowing that one day I’ll want /comments?user=15
, I prefer to handle both listings at the non-nested route. Instead of /articles/16/comments
, I’ll use /comments?article=16
. It’s not as pretty, but it’s simple, follows REST, and has a lot of flexibility.
Non-RESTful Routes
Using a non-RESTful approach is not recommended, but you can do it if the need arises.
In routes.rb
you’d call the match
method and define a pattern like this:
1 2 3 |
|
That would work for triggering just the show
action for the given ID. Or, to go all in, you can use the this pattern:
1
|
|
That will take the controller, action, and ID from the URL. This is a really bad plan. First, it gives you no structure and allows you to write actions with whatever naming conventions you come up with. It also makes all controller actions trigger-able with a GET request.
The Dangers of GET
Imagine you write a Wiki using this non-RESTful route. Pages have delete links, but they have a JavaScript pop-up that says "Are you sure you want to delete?" and you trust your users. So it seems ok for now, right?
Then a Google spider comes along, it ignores JavaScript, and clicks every link on your page. Including your delete links. Goodbye all content! This has happened before. Don’t let it happen to you!
Route Priority
Rails’ router will use the first route it matches, ignoring all of the others. If you have multiple route definitions that could match a given request, put the more general route below the more specific.
Special Routes
Often a few special routes are helpful when developing a customer-facing application.
Type-able URLs
When an app supports authentication, you might add routes like this:
1 2 3 4 5 |
|
There are still the normal RESTful routes for sessions, but now there are the additional convenience routes /login
and /logout
. In addition, the :as
parameter gives them a name to use with the helper. In your app you can now refer to login_path
and logout_path
in addition to new_session_path
. Run rake routes
and it’d show these:
Terminal
|
Root Route
What should the user see when they go to the root of your site? This trips up many newcomers.
The critical step 1 is to delete the public/index.html
file. If a file in /public/
matches the request coming in to your app that request will never actually hit the router. As long as that Rails’ boilerplate "Welcome Aboard!" page exists, you cannot map the site root to any controller.
Once that file is removed, define the special root
route like this:
1 2 3 |
|
The right side uses a new syntax in Rails 3: "controller_name#action_name"
. Then run rake routes
and you’d see this:
Terminal
|
|
In the app you can now utilize the root_path
helper and it’ll work!
Redirection
One last technique that many developers miss out on: If you’re working on a public app it’s common that, while the app grows, the URL structure changes. But you don’t want to break any old URLs out there on the web nor squander whatever Google Rank those pages have built up.
The solution is to write redirection routes. Imagine that our articles used to be at /posts/
but now they’re at /articles/
. When a user requests /posts/16
we know they really want /articles/16
. Our first instinct might be to write this:
1 2 3 4 |
|
And that would work just fine. But when the user visit /posts/16
the URL will say posts
but the content comes from /articles/16
. The link is not broken, but it’s dividing your Google Rank between the two URLs. Instead, you want /posts/16
to give back an HTTP 302 Redirect message. Write the route like this:
1 2 3 4 |
|
Unfortunately you can’t use the articles_path
helper within the router itself, so we have to manually create that redirection string. But now when a user or bot visits the old URL they’ll be redirected to the new one.
Exercises
Let’s try out a few exercises to practice the router techniques.
Setup
You really don’t need much of an app to test routes. Let’s create a simple app and single controller from the terminal:
You should have rails installed, but if not, type: gem install rails -v '~>3.0.0'
to get the latest 3.0.x version.
Terminal
$ $ $ $ |
|
You can test a route like articles_path
within the console by executing app.articles_path
. Note that after you make changes to routes.rb
, you need to call reload!
in your console to refresh the route definitions.
Open a second terminal window and change to your project directory. Here you can run rake routes
as you make changes to view the routing table.
The Basics
Hop into the routes.rb
and implement each of the route techniques below.
- Add a
resources
declaration for a resource namedcompanies
. Observe that seven routes are added following the RESTful convention - Add a second set of resources named
managers
and observe the routes increase to 14- Extra: Condense the two
resources
lines into one that still generates all 14 routes. Note: You’ll need to undo this for some of the later exercises
- Extra: Condense the two
- Add nested
evaluations
resources underneathemployees
. Make sure that you have routes generated likeemployee_evaluations_path
- Extra: Experiment in the console with evaluating these nested routes. What parameters do they require?
- Add nested
scores
resources underneathevaluations
. Observe how the route names get insane, and reflect on how these nested resources are just not worth it.
Customizing REST
Now let’s go beyond the standard REST setup:
- Add a custom route that will trigger the
promote
action ofEmployeesController
when aPUT
is submitted topromote_employee_path
- Add a custom route that will trigger the
generate_statistics
action of ‘ManagersController’ when aGET
is submitted togenerate_statistics_managers_path
- In the console, try calling
app.employees_path(maximum_age: 30)
and look at the generate URL. What does this tell you about extra parameters in calls to route helpers?- Extra: Experiment with some parameters of your own creation, and try more than one at a time.
Non-RESTful Routes
Then a few simple ones:
- Add a route that will redirect requests for
/bosses/
to/managers/
- Add another that redirects
show
requests like/bosses/16
to/managers/16
- Add a route named
directory
that points to theindex
action ofEmployeesController
- Add a route named
search
that points to thenew
action of theSearchesController
- Extra: Modify this route so
/search/managers/fred
would trigger the same action/controller, but setmanagers
into a parameter namedgroup
andfred
into a parameter namedname
- Extra: Modify this route so
- Define the
root
route to display theindex
action ofManagersController
Solutions
For a complete solution to all of the above, visit this Gist: https://gist.github.com/1044122
Further Study
The most comprehensive and up-to-date source on all things Routing is the Rails Guide: http://guides.rubyonrails.org/routing.html