Restful Routing: An Overview

January 6th, 2008 by Radar

Here’s something I posted on rubyonrails talk:

In truth, restful routing is plain and simple. It’s like those books you wrote when you were a kid in kindergarten that if a book critic were to read them he would jab his eyes out with a pen.

For this example I’m going to use a personal favourite: a forum system. It’s small enough to not be overwhelming, yet large enough to explain how restful routing should work (and generally why you should use it).

It all starts with the good ‘ol config/routes.rb file. In here is where all the nice little routes live, from map.root all the way down to the map.connect ‘:controller/service.wsdl’, :action => ‘wsdl’ that many people still leave in their routes file, thinking that if they remove it the entire world would collapse upon itself into one small quantum singularity. In here you’d place something similar to:

map.resources :forums, :hasmany => :topics
map.resources :topics, :hasmany => :posts
map.resources :posts
Not running on Rails 2.0? Then this is the code you want:
map.resources :forums do |forum|
  forum.resources :topics, :nameprefix => "forum"
end

map.resources :topics do |topic| topic.resources :posts, :nameprefix => "topic" end

map.resources :posts

This defines routes like:
/forums/1
- Show a forum
/forums/1/topics
- Index action for a single forum
/forums/1/topics/1
- showing a single topic
/topics/1
- Same thing
/topics/1/posts/
- I would imagine this would do a similar thing to /topics/1
/topics/1/posts/1/edit
- Allows you to edit a single post
/posts/1/edit
- Same thing

Now to define something like this without the magic of restful routing, one would have to be clinically insane:

map.connect "/forums/:id", :controller => "forums", :action => "show"
map.connect "/forums/:forumid/topics/", :controller => "topics", :action => "index"
map.connect "/forums/:forumid/topics/:id", :controller => "topics", :action => "show"
map.connect "/topics/:id", :controller => "topics", :action => "show" <- Does this look familar to /forums/:id?
map.connect "/topics/:topic_id/posts", :controller => "posts", :action => "index"
map.connect "/topics/:topic_id/posts/:id/edit", :controller => "posts", :action => "edit"
map.connect "/posts/:id/edit", :controller => "posts", :action => "edit"
And this is only the tip of the iceberg!

Seeing a pattern here? Restful routing gives you a whole heap of cool stuff, namely the 7 core methods that I’ll cover right after the models.

A forum system has the following tables: forums, topics, posts and users, and the models would look something like the following:

class Forum < ActiveRecord::Base
  hasmany :topics
  hasmany :posts, :through => :topics
end

class Topic < ActiveRecord::Base
  hasmany :posts
  belongsto :forum
  belongs_to :user
end


class Post < ActiveRecord::Base
  belongsto :topic
  belongsto :user
end


class User < ActiveRecord::Base
  hasmany :topics
  hasmany :posts
  def to_s
    login
  end
end
The fields don’t matter, but throughout the tutorial I make reference to @forum.name or something similar, so we’ll assume forums has at least a name field. We’ll assume post has a text field and users has a login field.

That’ll give you some idea of how the system works: Forum -> Topics -> Posts.

In restful routing there are seven “core” methods (actions) that you’re given for the controllers: index, show, new, create, edit, update, destroy. Each of these have a set request method on them, for example you can’t GET to the create, update and destroy actions and you can’t post to the index, new or edit actions. These actions work with these request methods:

GET: index, show, new, edit POST: create PUT: update DELETE: destroy

“What the?! PUT & DELETE, where did they come from?”, I hear you cry! These are hacked into the calls for the appropriate action using javascript, it passes in one more parameter (_method) which is then handled by the rails code and depending on what method you called you will get the page you were looking for, or a routing error.

The forums controller could look like this:

class ForumsController < ApplicationController
  def index
    @forums = Forum.find(:all)
  end

def show @forum = Forum.find(params[:id]) end

def new @forum = Forum.new end

def create @forum = Forum.new(params[:forum]) if @forum.save flash[:notice] = "You have created a forum!" redirectto forumspath else render :action => "new" end end

def edit @forum = Forum.find(params[:id]) end

def update @forum = Forum.find(params[:id]) if @forum.updateattributes(params[:forum]) flash[:notice] = "You have updated #{@forum.name}" redirectto forum_path else flash[:error] = "This forum could not be updated." render :action => "new" end end

def destroy @forum = Forum.find(params[:id]) @forum.destroy flash[:notice] = "You have deleted #{@forum.name}" redirectto forumspath end

end

You’ll see here that I’ve twice made a call to redirectto using the argument of forumspath. Because we’ve defined map.resources :forums in our config/routes.rb file, it knows that we want to go to { :controller => “forums”, :action => “index” } and the best part is that we don’t have to keep trying { :controller => “forums”, :action => “index” } every time we want to go to that specific action, but instead we type forums_path.

I’ve also made a single call to forum_path, and I haven’t specified an argument for it, so how does Rails know that I want to go to the forum that I just updated?

Rails will see that there’s an argument mission from the forum_path and will go looking for the @forum instance variable you’ve defined in your controller. If you never defined one or defined it as something other than @forum, it will mention something about ambiguous routes and you’ll have to specify the variable.

Now what if you wanted to go to the new or edit action? Simple: newforumpath and editforumpath(@forum) will take you to the corresponding actions. Remember that you don’t need to specify an argument for the editforumpath if @forum is defined. Inside these actions you’ll want to go further, you’ll want to create a new forum and update a forum.

For the create action you could specify this for your form: Rails 2.0:

<% form_for @forum do |f|%>
Rails 2.0 will see that @forum is a new record and link you the create action.

Pre Rails 2.0:

<% form_for :forum, @forum, :url => forums_path do |f|%>
Prior to Rails 2.0 that checking wasn’t in, you’ll have to define your own link.

“B-b-b-ut”, you stammer, “you’ve linked to the forums index, right? Isn’t that what forums_path is?”

Well, yes and no. This has everything to do with the four request methods mentioned previously, because the form’s method attribute is “post”, Rails knows that if you’re posting to forums_path, you mean the create action. And now for the update action!

Here the form_for’s a little different, but only for pre-Rails 2.0:

Rails 2.0:

<% form_for @forum do |f| %>
Again the same deal applies: Rails 2.0 knows that @forum is not a new record, so it’ll link you to to the update action because it’s included in a form. This automatically specifies :html => { :method => :put } for you.

Pre Rails 2.0

<% form_for :forum, @forum, :url => forum_path(@forum), :html => { :method => "put" } do |f| %>
It knows to link you to the update action because of the method => “put” we’ve specified.

Now lets escape from the confines of a single controller and bring the topics controller into the mix. In the forum show action is where you would generally show all the topics for that forum, but for the purposes of this tutorial I will do it in the topics controller instead. This will have something similar defined to the forums controller but personally I would define this for the controller:

class TopicsController < ApplicationController
  beforefilter :getforum

def index @topics = @forum.topics end

#other actions private def getforum @forum = Forum.find(params[:forumid]) if params[:forum_id] end end

The private call makes any method after it private, that means that if you were to try and access this method (without restful routing), it would play dumb. Personally, I think something like this should be in-built to Rails, if you’re accessing a child object (topics) from a parent (forum) it should automatically define @forum for you.

Because we’ve already defined the :hasmany topics on map.resources :forums, the topic routes are already defined for us, so to view all the topics for a forum, before you would have to do define a route like this:

map.connect "/forums/:forumid/topics/", :controller => "topics", :action => "index"
and then call it like this:
{ :controller => "topics", :action => "index", :forumid => @forum.id }
Instead you’ve already defined :hasmany => :topics, so instantly you’ll gain access to forumtopicspath. Again the wonderful Rails will realise that you want all topics for the @forum object and then direct you to “/forums/1/topics/” through forumtopicpath. To edit a single topic, you could do editforumtopicpath as of Rails 2.0, or forumedittopicpath prior to Rails 2.0. The first reads more like “edit this topic belonging to this forum” where the second reads like “in this forum, edit this topic”. Alternatively you could ditch the whole forum part out of the method call and just do edittopicpath because we’ve defined map.resources :topics.

Throwing one more controller into the mix now, called postscontroller. This would be very similar to the topics controller but instead of getforum it would have get_topic, modified correctly.

Now what if you wanted to add a custom action to posts_controller, called quote? This action would bring up a form with the post you were quoting which would then send the information from the form into posts/new to create a new post:

config/routes.rb

map.resources :posts, :member => { :quote => :get }
The extra argument of :member indicates a hash of any further actions and their request methods you would like to be added onto singular posts. You can call these like all the other singular methods: quotepostpath(@post), for example. The request methods can be in string or symbol format, it doesn’t matter.
def quote
  @oldpost = Post.find(params[:id])
  @post = Post.new
  @post.text = "[quote='#{@oldpost.user}']#{@old_post.text}[/quote]"
  render :action => "new"
end
Here I’ve defined the old post only to get the text and the user’s name from it, and then we’re rendering the new view so we have the form. Everything from there on is taken care by Rails.

Now what if you want to define a new action to work with a group of posts? Well you define it like this: routes.rb

map.resources :posts, :member => { :quote => :get }, :collection => { :destroyall => :delete }
Now if you wanted to destroy all posts for a topic, bar the first one, with an action like this (remembering @topic is defined in gettopic): postscontroller.rb
def destroyall
  @posts = @topic.posts  - @topic.posts.first
  @posts.each { |post| post.destroy }
  flash[:notice] = "All posts have been deleted.
end
You would linkto it like this:
<%= link_to "Delete all posts", destroy_all_topic_posts_path(@topic), :method => "delete", 
:confirm => "Are you sure you want to delete all posts from this topic?" %>
The :method => “delete” corresponds with the :delete
all => :delete we specified in config/routes.rb.

One last thing is nested form_fors. Say you want to edit a post within a topic. To do this you would use this code:

<% form_for [@topic, @post] do |f| %>
  <%= render :partial => "form", :locals => { :f => f } %>
  <%= submit_tag "Update" %>
<% end %>

Here you pass one argument still to form_for, an array. It passes the array and will give you a url like /topics/1/posts/1 if you were editing an existing post, or /topics/1/posts if you were creating a new post.

Tags: ,

18 Responses to “Restful Routing: An Overview”

  1. Nathan Says:

    Hey Ryan, thanks for following my advice and posting this on a blog. Now it will be easier for people to read and find in the future. It is a good resource. I wish more tutorials were written on how to ACTUALLY implement RESTful applications. I am tired of seeing people merely state how great rest is and then explain that there are 7 actions, yadda yadda. I want to see actual explanations on how to tackle applications using REST in Rails 2.

  2. debianfreak Says:

    I’ve finished reading your article. It explained a lot – I had previously not understood setting up routes in such a way. It seems like there is always more routing stuff to learn… anyway, I tried to implement part of your article and failed. Specifically, my Person -> Messages is like your Forum -> Topics, so I did this route: map.people, :hasmany => :messages and then in the show view for people I did linkto ‘Messages’, personmessagespath. However, this failed (failed to generate personmessagesurl). I looked through the whole article again and tried adding a before_filter to my messages controller to get the person and assign it to @person, but no joy. Any idea what I am missing? :S

  3. debianfreak Says:

    Whooops, in my above messages I meant to say that I did map.resources :people, :has_many => :messages

  4. debianfreak Says:

    Ok… I figured it out. I needed to use linkto ‘Messages’, personmessages_path(@person) and then it works :) I didn’t see that mentioned… perhaps you should add it?

    Great article though – it helped me a lot!

  5. Jose Espinal Says:

    Great great article! You don’t know how grateful I am for it, explained a lot of holes I had about REST.

  6. The Life Of A Radar » Blog Archive » Administration Namespacing Says:

    [...] so I figured if I sat down and wrote this, I would have something to send to people, much like my Restful Routes tutorial, which ideally should’ve covered namespacing [...]

  7. zeroeth Says:

    Hey Radar!

    Great post about nested resources. I see lots of people asking and I only ever have one link to show them, this makes two =)

  8. Tom Says:

    Good post!

    Worthwhile as well: http://api.rubyonrails.com/classes/ActionController/Resources.html

    Interestingly, your post seems to be more advanced in some points than the API description…

  9. Hadmut Says:

    Hi,

    sorry to contradict, but restful routing is neither plain nor simple. In my eyes it is just bad design.

    If it were plain and simple, it would not require such explanatory articles.

    I am just busy with porting a rails application to rails 2.0 and found that this restful routing breaks backwards compatiblity. Old views don’t work with new views generated with scaffolding. Formerly the paths were of the simple structure /:controller/:action/:id, but now they don’t have any consistent structure anymore.

    Formerly, functions like redirect_to could take variables as parameters for controller, action and id, but now I don’t see a way to do this consistently anymore.

    In my eyes, the restful routing is of poor design and shortsighted. It just causes confusion and makes things more complicated.

    Was there any good reason to introduce it?

  10. Gudasoft » Blog Archive » Rails 2.0 scaffolding Says:

    [...] http://frozenplague.net/2008/01/06/restful-routing-an-overview/ [...]

  11. Radar Says:

    The Restful routing syntax is designed to be easier to use, and yes of course it’s not going to be compatible with previous versions of Rails (what is? :)). The reason why I prefer to use it is, instead of typing link_to “New Blog”, :controller => “blogs”, :action => “new” I can now type < %= link_to "New Blog", new_blog_path %>, or even < %= link_to "New Blog", @blog %> if @blog is defined as Blog.new.

    Yes, it will be a pain bringing those older views in line with the new syntax but once you’ve done it you get a consistent, easy to use syntax across everything.

    How were you specifying variables in your redirect_to before?

  12. Brenton Says:

    Very funny post! If only I’d found this when I was learning REST.

  13. Pan Says:

    Thanks for writing such a helpful article. I’ve learned really much from it.

  14. Walter Says:

    Thanks for an excellent article. I’m a beginner. Do you have the code for this example? If so, would you post it here or mail it to me please? Thanks in advance. Regards. Walter.

  15. Radar Says:

    The code for this can be found at http://github.com/radar/rboard.

  16. Unrestful Says:

    I’m a n00b at rails, and have this situation I’d like to resolve with restful routes. I’ve defined a metod “back” in application.rb which goes back in browser history. All controllers benefits from this.

    Is there a restful way to accomplish the same?

    Regards.

  17. Radar Says:

    Unrestful:

    I can’t think of anyway, sorry.

  18. ngw Says:

    Unrestful, don’t reimplement redirect_to :back :)

Leave a Reply