6.6. resources

resources provides routes for a RESTful resource. Let's try it with the mini blog application:
$ rails new blog
  [...]
$ cd blog
$ rails generate scaffold Post subject content published_at:date
  [...]
$ rake db:migrate
  [...]
$
The scaffold generator automatically creates a resources route in the config/routes.rb:
Blog::Application.routes.draw do
  resources :posts
end

Note

New routes are always added at the beginning of config/routes.rb by rails generate scripts.
The resulting routes:
$ rake routes
    posts GET    /posts(.:format)          posts#index
          POST   /posts(.:format)          posts#create
 new_post GET    /posts/new(.:format)      posts#new
edit_post GET    /posts/:id/edit(.:format) posts#edit
     post GET    /posts/:id(.:format)      posts#show
          PUT    /posts/:id(.:format)      posts#update
          DELETE /posts/:id(.:format)      posts#destroy
$
You have already encountered these RESTful routes in Chapter 5, Scaffolding and REST. They are required for displaying and editing records.

Selecting Specific Routes with :only or :except

If you only want to use specific routes from the finished set of RESTful routes, you can limit them with :only or :except.
The following conf/routes.rb defines only the routes for index and show:
Blog::Application.routes.draw do
  resources :posts, :only => [:index, :show]
end
With rake routes we can check the result:
$ rake routes
posts GET /posts(.:format)     posts#index
 post GET /posts/:id(.:format) posts#show
$ 
except works exactly the other way round:
Blog::Application.routes.draw do
  resources :posts, :except => [:index, :show]
end
Now all routes except for index and show are possible:
$ rake routes
    posts POST   /posts(.:format)          posts#create
 new_post GET    /posts/new(.:format)      posts#new
edit_post GET    /posts/:id/edit(.:format) posts#edit
     post PUT    /posts/:id(.:format)      posts#update
          DELETE /posts/:id(.:format)      posts#destroy
$

Warning

When using only and except, please make sure you also adapt the views generated by the scaffold generator. For example, there is a link on the index page to the new view with <%= link_to 'New Post', new_post_path %> but this view no longer exists in the above only example.

Nested Resources

Nested resources refer to routes of resources that work with a has_many association (see Section 4.8, “has_many – 1:n Association”). These can be addressed precisely via routes. Let's create a second resource, comment:
$ rails generate scaffold comment post_id:integer content
  [...]
$ rake db:migrate
  [...]
$ 
Now we associate the two resources. In the file app/models/post.rb, we add a has_many:
class Post < ActiveRecord::Base
  attr_accessible :content, :published_at, :subject

  has_many :comments
end
And in the file app/models/comment.rb, its counterpart belongs_to:
class Comment < ActiveRecord::Base
  attr_accessible :content, :post_id

  belongs_to :post
end
The routes generated by the scaffold generator look like this:
$ rake routes
    comments GET    /comments(.:format)          comments#index
             POST   /comments(.:format)          comments#create
 new_comment GET    /comments/new(.:format)      comments#new
edit_comment GET    /comments/:id/edit(.:format) comments#edit
     comment GET    /comments/:id(.:format)      comments#show
             PUT    /comments/:id(.:format)      comments#update
             DELETE /comments/:id(.:format)      comments#destroy
       posts GET    /posts(.:format)             posts#index
             POST   /posts(.:format)             posts#create
    new_post GET    /posts/new(.:format)         posts#new
   edit_post GET    /posts/:id/edit(.:format)    posts#edit
        post GET    /posts/:id(.:format)         posts#show
             PUT    /posts/:id(.:format)         posts#update
             DELETE /posts/:id(.:format)         posts#destroy
$ 
So we can get the first post with /posts/1 and all the comments with /comments. By using nesting, we can get all comments with the ID 1 via /posts/1/comments. We need to change the config/routes.rb:
Blog::Application.routes.draw do
  resources :posts do
    resources :comments
  end
end
This gives us the desired routes:
$ rake routes
    post_comments GET    /posts/:post_id/comments(.:format)          comments#index
                  POST   /posts/:post_id/comments(.:format)          comments#create
 new_post_comment GET    /posts/:post_id/comments/new(.:format)      comments#new
edit_post_comment GET    /posts/:post_id/comments/:id/edit(.:format) comments#edit
     post_comment GET    /posts/:post_id/comments/:id(.:format)      comments#show
                  PUT    /posts/:post_id/comments/:id(.:format)      comments#update
                  DELETE /posts/:post_id/comments/:id(.:format)      comments#destroy
            posts GET    /posts(.:format)                            posts#index
                  POST   /posts(.:format)                            posts#create
         new_post GET    /posts/new(.:format)                        posts#new
        edit_post GET    /posts/:id/edit(.:format)                   posts#edit
             post GET    /posts/:id(.:format)                        posts#show
                  PUT    /posts/:id(.:format)                        posts#update
                  DELETE /posts/:id(.:format)                        posts#destroy
$ 
But we still need to make some changes in the file app/controllers/comments_controller.rb. This ensures that only the Comments of the specified Post can be displayed or changed (to make it clearer, I have removed the JSON part):
class CommentsController < ApplicationController
  # We use a before_filter to find the @post.
  #
  before_filter :find_the_post

  # GET /posts/1/comments
  def index
    @comments = @post.comments
  end

  # GET /posts/1/comments/1
  def show
    @comment = @post.comments.find(params[:id])
  end

  # GET /posts/1/comments/new
  def new
    @comment = @post.comments.build
  end

  # GET /posts/1/comments/1/edit
  def edit
    @comment = @post.comments.find(params[:id])
  end

  # POST /posts/1/comments
  def create
    @comment = @post.comments.build(params[:comment])

    respond_to do |format|
      if @comment.save
        format.html { redirect_to [@post, @comment], notice: 'Comment was successfully created.' }
      else
        format.html { render action: "new" }
      end
    end
  end

  # PUT /posts/1/comments/1
  def update
    @comment = @post.comments.find(params[:id])

    respond_to do |format|
      if @comment.update_attributes(params[:comment])
        format.html { redirect_to [@post, @comment], notice: 'Comment was successfully updated.' }
      else
        format.html { render action: "edit" }
      end
    end
  end

  # DELETE /posts/1/comments/1
  def destroy
    @comment = @post.comments.find(params[:id])
    @comment.destroy

    respond_to do |format|
      format.html { redirect_to post_comments_path(@post) }
    end
  end

  private
  def find_the_post
    @post = Post.find(params[:post_id])
  end
end
Unfortunately, this is only half the story, because the views still link to the old routes. So we need to adapt each view in accordance with the nested route.
app/views/comments/_form.html.erb
Please note that you need to change the form_for call to form_for([@post, @comment]).
<%= form_for([@post, @comment]) do |f| %>
  <% if @comment.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:</h2>

      <ul>
      <% @comment.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :content %><br />
    <%= f.text_field :content %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
app/views/comments/edit.html.erb
<h1>Editing comment</h1>

<%= render 'form' %>

<%= link_to 'Show', [@post, @comment] %> |
<%= link_to 'Back', post_comments_path(@post) %>
app/views/comments/index.html.erb
<h1>Listing comments</h1>

<table>
  <tr>
    <th>Post</th>
    <th>Content</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @comments.each do |comment| %>
  <tr>
    <td><%= comment.post_id %></td>
    <td><%= comment.content %></td>
    <td><%= link_to 'Show', [@post, comment] %></td>
    <td><%= link_to 'Edit', edit_post_comment_path(@post, comment) %></td>
    <td><%= link_to 'Destroy', [@post, comment], method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New Comment', new_post_comment_path(@post) %>
app/views/comments/new.html.erb
<h1>New comment</h1>

<%= render 'form' %>

<%= link_to 'Back', post_comments_path(@post) %>
app/views/comments/show.html.erb
<p id="notice"><%= notice %></p>

<p>
  <b>Post:</b>
  <%= @comment.post_id %>
</p>

<p>
  <b>Content:</b>
  <%= @comment.content %>
</p>


<%= link_to 'Edit', edit_post_comment_path(@post, @comment) %> |
<%= link_to 'Back', post_comments_path(@post) %>
Please go ahead and have a go at experimenting with the URLs listed under rake routes. You can now generate a new post with /posts/new and a new comment for this post with /posts/:post_id/comments/new.
If you want to see all comments of the first post you can access that with the URL http://0.0.0.0:3000/posts/1/comments. It would look like this:
Nested comments

Comments on Nested Resources

Generally, you should never nest more deeply than one level and nested resources should feel natural. After a while, you will get a feel for it. In my opinion, the most important point about RESTful routes is that they should feel logical. If you phone a fellow Rails programmer and say "I've got a resource post and a resource comment here", then both parties should immediately be clear on how you address these resources via REST and how you can nest them.

Updates about this book will be published on my Twitter feed.