Be thee warned, this is a *long* article. However, it covers a lot of ground, is written in plain english, and comes with tons of code samples to make the subject matter as easy to digest as possible.
Routing has changed in Rails 3. It’s very different from 2.x, but it’s not actually difficult. This is one of those things where new users are almost better off, because they’re learning the simple way right off the bat, with no baggage. Whether you’re new to all of Rails or just Rails 3, this will get you up and running.
Static-y routes
Starting off with a fresh rails app, let’s generate our main static controller:
rails3_routing$ rails g controller home index about
create app/controllers/home_controller.rb
route get "home/about"
route get "home/index"
invoke erb
create app/views/home
create app/views/home/index.html.erb
create app/views/home/about.html.erb
invoke test_unit
create test/functional/home_controller_test.rb
invoke helper
create app/helpers/home_helper.rb
invoke test_unit
create test/unit/helpers/home_helper_test.rb
We can clean up our routes file so it looks something like this:
Rails3Routing::Application.routes.draw do get "home/index" get "home/about" root :to => "home#index" end
The controller generator created the two static routes for us, and I made home/index the root (default) route for the application. Now I need to get rid of the default index file in the public directory:
rails3_routing$ ls public 404.html 500.html images javascripts stylesheets 422.html favicon.ico index.html robots.txt rails3_routing$ rm public/index.html rails3_routing$ ls public 404.html 500.html images robots.txt 422.html favicon.ico javascripts stylesheets
If we don’t remove this, the app will show the public/index.html file as the home page. So far so good! If you fire up the server, you’ll see our home/index action is being called as our home page.
RESTful Routes
Let’s use the example of a blog application, and create the Post model, views, and controller to look at RESTful routing in Rails 3. We’ll use the scaffold generator to create the entire MVC (Model-View-Controller) stack in one shot:
rails3_routing$ rails g scaffold post title:string content:text
invoke active_record
create db/migrate/20101202051750_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/unit/post_test.rb
create test/fixtures/posts.yml
route resources :posts
invoke scaffold_controller
create app/controllers/posts_controller.rb
invoke erb
create app/views/posts
create app/views/posts/index.html.erb
create app/views/posts/edit.html.erb
create app/views/posts/show.html.erb
create app/views/posts/new.html.erb
create app/views/posts/_form.html.erb
invoke test_unit
create test/functional/posts_controller_test.rb
invoke helper
create app/helpers/posts_helper.rb
invoke test_unit
create test/unit/helpers/posts_helper_test.rb
invoke stylesheets
create public/stylesheets/scaffold.css
Looking at our routes.rb file, you can see we now have our first RESTful route, added automatically by Rails:
Rails3Routing::Application.routes.draw do resources :posts get "home/index" get "home/about" root :to => "home#index" end
This doesn’t look like much – it only added a line to our routes file – but it packs a punch. For starters, it’s defining the seven basic RESTful routes:
- GET index
- GET show
- GET new
- POST create
- GET edit
- PUT update
- DELETE destroy
These are the seven routes needed to list, view, add, update, and remove posts in our application. But what if we want to add custom routes to this controller? Maybe we want a couple actions called recent that shows only the latest 5 posts, and popular that shows the top 5 popular posts. Let’s also add an action called publish that will activate a post once it’s ready, so people can see it. Let’s add these to our routes:
resources :posts do
get 'recent', :on => :collection
get 'popular', :on => :collection
put 'publish', :on => :member
end
Both recent and popular use http GET requests, because we’re not changing any data on the server. The publish action uses the http PUT request, to denote a route that does changes records on the server.
The publish action is a member action, meaning when we call this action, the route will have to contain the id of the record we want to work with, like this: “/posts/1/publish”. The other two actions are collection-oriented, meaning they work with the collection as a whole and we don’t need to pass a specific id to them. The recent route would look like “/posts/recent”.
When you start adding routes, the notation above may get tedious. Luckily, there’s a shortcut:
resources :posts do
collection do
get 'recent'
get 'popular'
end
member do
put 'publish'
end
end
This is the preferred way to notate custom routes, whether you have many or just a couple. It’s easier to read, and easier to update with more routes in the future.
Nested Routes
Now let’s say we want to add comments to our application. Obviously every comment will be attached to a specific post, and we’d like the routes to reflect this. It’s not absolutely necessary, but it does add much needed structure in these trying and uncertain times. Let’s generate our comments:
rails3_routing$ rails g scaffold comment title:string content:text post_id:integer
invoke active_record
create db/migrate/20101202054608_create_comments.rb
create app/models/comment.rb
invoke test_unit
create test/unit/comment_test.rb
create test/fixtures/comments.yml
route resources :comments
invoke scaffold_controller
create app/controllers/comments_controller.rb
invoke erb
create app/views/comments
create app/views/comments/index.html.erb
create app/views/comments/edit.html.erb
create app/views/comments/show.html.erb
create app/views/comments/new.html.erb
create app/views/comments/_form.html.erb
invoke test_unit
create test/functional/comments_controller_test.rb
invoke helper
create app/helpers/comments_helper.rb
invoke test_unit
create test/unit/helpers/comments_helper_test.rb
invoke stylesheets
identical public/stylesheets/scaffold.css
Now our routing file will have our comments routes, but they’re not nested inside our posts routes. Let’s fix that:
resources :posts do
resources :comments
collection do
get 'recent'
get 'popular'
end
member do
put 'publish'
end
end
Easy! We just moved the resource line into our posts block. In order to complete the process, I’ve added the necessary relationships in the models, before filter in the comments controller (to lookup the parent post) and updated the comments controller to perform all comment lookups from the perspective of the parent post. Feel free to view the source code for this article (link at the top of the page) to see how I did this.
Now if you’re editing a comment with id 100 that belongs to post 20, the route will look like: “/posts/20/comments/100/edit”.
Namespaced Routes
After a while, you might realize that your posts and comments need to be treated differently depending on the context. One way to do this is with namespacing. For example, you might want to create an admin section of the site where posts are shown with more options (re-ordering, editing, etc). While I won’t get into the permissions issues in this article, this is easy to do from a routing perspective.
First, we’ll create the namespaced controller:
rails3_routing$ rails g controller admin/posts index show new create edit update destroy
create app/controllers/admin/posts_controller.rb
route get "posts/destroy"
route get "posts/update"
route get "posts/edit"
route get "posts/create"
route get "posts/new"
route get "posts/show"
route get "posts/index"
invoke erb
create app/views/admin/posts
create app/views/admin/posts/index.html.erb
create app/views/admin/posts/show.html.erb
create app/views/admin/posts/new.html.erb
create app/views/admin/posts/create.html.erb
create app/views/admin/posts/edit.html.erb
create app/views/admin/posts/update.html.erb
create app/views/admin/posts/destroy.html.erb
invoke test_unit
create test/functional/admin/posts_controller_test.rb
invoke helper
create app/helpers/admin/posts_helper.rb
invoke test_unit
create test/unit/helpers/admin/posts_helper_test.rb
As you can see, calling the controller “admin/posts” caused all the newly created files to be put in their own admin directory. If we look in the routes file, we’ll see this ugliness:
get "posts/index" get "posts/show" get "posts/new" get "posts/create" get "posts/edit" get "posts/update" get "posts/destroy"
The controller generator doesn’t know that we want RESTful, namespaced routes, so we’ll drop all those lines in favor of this:
namespace :admin do
resources :posts
end
Note that we’re adding posts resources again, but this time they’re namespaced. They won’t interfere with our regular posts routes, because these routes will be prefixed with “/admin”. So editing the post with id “100″ in the admin section will have a route like this: “/admin/posts/100/edit”.
Things You Should Know
While our initial routes file is pretty simple and straightforward, it would quickly grow if this were a real app. How does the Rails 3 routing engine choose from conflicting or overlapping routes? It does this the same way Rails 2.x does – it works top to bottom. It does not try to find the *best* match (the way a google search might) – instead, it starts at the top of the routes file and stops when it finds the first acceptable match. Therefore, default routes should be pushed to the bottom.
While we’re on the subject, don’t use default routes. Named routes form a much more secure and stable application. There shouldn’t be routes you’re NOT expecting. One exception can be short urls. If you want members to have a short url the way Twitter does (http://twitter.com/kconrails for instance) then you’ll need a default route, placed at the bottom of the routes file, to be tried after all other routes have been checked. If you had a members controller, and the member name should route to the show action, the route might look like this:
match ':member' => 'members#show', :as => :shorturl
Finally, when in doubt, check your existing routes with this rake task:
rails3_routing$ rake routes
(in /Users/bellmyer/Desktop/bellmyer/blog/rails3_routing)
admin_posts GET /admin/posts(.:format) {:action=>"index", :controller=>"admin/posts"}
admin_posts POST /admin/posts(.:format) {:action=>"create", :controller=>"admin/posts"}
new_admin_post GET /admin/posts/new(.:format) {:action=>"new", :controller=>"admin/posts"}
edit_admin_post GET /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"}
admin_post GET /admin/posts/:id(.:format) {:action=>"show", :controller=>"admin/posts"}
admin_post PUT /admin/posts/:id(.:format) {:action=>"update", :controller=>"admin/posts"}
admin_post DELETE /admin/posts/:id(.:format) {:action=>"destroy", :controller=>"admin/posts"}
post_comments GET /posts/:post_id/comments(.:format) {:action=>"index", :controller=>"comments"}
post_comments POST /posts/:post_id/comments(.:format) {:action=>"create", :controller=>"comments"}
new_post_comment GET /posts/:post_id/comments/new(.:format) {:action=>"new", :controller=>"comments"}
edit_post_comment GET /posts/:post_id/comments/:id/edit(.:format) {:action=>"edit", :controller=>"comments"}
post_comment GET /posts/:post_id/comments/:id(.:format) {:action=>"show", :controller=>"comments"}
post_comment PUT /posts/:post_id/comments/:id(.:format) {:action=>"update", :controller=>"comments"}
post_comment DELETE /posts/:post_id/comments/:id(.:format) {:action=>"destroy", :controller=>"comments"}
recent_posts GET /posts/recent(.:format) {:action=>"recent", :controller=>"posts"}
popular_posts GET /posts/popular(.:format) {:action=>"popular", :controller=>"posts"}
publish_post PUT /posts/:id/publish(.:format) {:action=>"publish", :controller=>"posts"}
posts GET /posts(.:format) {:action=>"index", :controller=>"posts"}
posts POST /posts(.:format) {:action=>"create", :controller=>"posts"}
new_post GET /posts/new(.:format) {:action=>"new", :controller=>"posts"}
edit_post GET /posts/:id/edit(.:format) {:action=>"edit", :controller=>"posts"}
post GET /posts/:id(.:format) {:action=>"show", :controller=>"posts"}
post PUT /posts/:id(.:format) {:action=>"update", :controller=>"posts"}
post DELETE /posts/:id(.:format) {:action=>"destroy", :controller=>"posts"}
home_index GET /home/index(.:format) {:controller=>"home", :action=>"index"}
home_about GET /home/about(.:format) {:controller=>"home", :action=>"about"}
root /(.:format) {:controller=>"home", :action=>"index"}
It will give you a list of all available routes that have been setup in your application. It also gives you the route names. The first word of each line is the route name. The first named route in our example is “admin_posts”. You can append “_url” or “_path” in views and controllers to get either that action’s url with or without the hostname, respectively.
Also note that in the routes listed above, any symbols that are NOT in parentheses are required. For example, on line 10 above, post_comments_path would require post_id to be passed in. You can do this with either the id, or the object itself. For example
post_comments_path(@post) post_comments_path(@post.id)
This is a lot to play with, which is why I like including code samples with articles like this. Download (link at the top of the page) and play around with the routes until you're comfortable. After and hour or so, you won't need to lookup the Rails specs every time you want to make a routing change.
Tags: controller, model, Rails, resources, restful, routing, Ruby, ruby on rails, view

May 4, 2011 at 6:58 pm |
When you’re using nested routes, make sure to change all form_for(@comment) to form_for([@post, @comment]) and all redirect_to(@comment) to redirect_to([@post, @comment])
It’s a bit different because we’ve got to pass a post object along with a comment object.
Also, link_to paths might need to be changed up, because we don’t have a comments_path, instead we have a post_comments_path, etc.
June 1, 2011 at 4:49 pm |
Hey bellmyer. First of all thank you very much for this post.
But as you may have guessed, almost nobody writes if everything ist fine. So to get it out: I looked at your github code and found out, that your controllers are structured in this nested way but your views are not.
Maybe this was your intention, anyway, i modified my comment’s index view to something like %= link_to ‘Show’, post_comment_path(comment.post.id, comment.id) % which is working… is this the way it should be done or am i wrong? i mean… sure i am showing a comment which is unique even without post.id, but then (with comment_path(comment.id)) there is an exeption “no controller found”, which is justified i guess.
i would be happy if you could clear my thoughts up a bit
June 1, 2011 at 5:28 pm |
ok this is not a problem anymore.
if anybody get the same troubles, you’ll have to edit your views to the new routes structure presented here.
eg. you’d have to write %= link_to ‘Destroy’, post_comment_path(comment.post.id, comment.id), :confirm => ‘Are you sure?’, :method => :delete % instead of %= link_to ‘Destroy’, comment, :confirm => ‘Are you sure?’, :method => :delete %
*** Disclaimer: I’m like 90% noob and may be 100% wrong ;)
August 6, 2011 at 11:05 pm |
Thanks. Helpful article