6.4. 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

Buy the new Rails 5.1 version of this book.

New routes are always added at the beginning of config/routes.rb by rails generate scripts.
The resulting routes:
$ rake routes
   Prefix Verb   URI Pattern               Controller#Action
    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
          PATCH  /posts/:id(.:format)      posts#update
          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
Prefix Verb URI Pattern          Controller#Action
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
   Prefix Verb   URI Pattern               Controller#Action
    posts POST   /posts(.:format)          posts#create
 new_post GET    /posts/new(.:format)      posts#new
edit_post GET    /posts/:id/edit(.:format) posts#edit
     post PATCH  /posts/:id(.:format)      posts#update
          PUT    /posts/:id(.:format)      posts#update
          DELETE /posts/:id(.:format)      posts#destroy
$

Buy the new Rails 5.1 version of this book.

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 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
  has_many :comments
end
And in the file app/models/comment.rb, its counterpart belongs_to:
class Comment < ActiveRecord::Base
  belongs_to :post
end
The routes generated by the scaffold generator look like this:
$ rake routes
      Prefix Verb   URI Pattern                  Controller#Action
    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
             PATCH  /comments/:id(.:format)      comments#update
             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
             PATCH  /posts/:id(.:format)         posts#update
             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
           Prefix Verb   URI Pattern                                 Controller#Action
    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
                  PATCH  /posts/:post_id/comments/:id(.:format)      comments#update
                  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
                  PATCH  /posts/:id(.:format)                        posts#update
                  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:
class CommentsController < ApplicationController
  before_action :set_post
  before_action :set_comment, only: [:show, :edit, :update, :destroy]

  # GET /comments
  # GET /comments.json
  def index
    @comments = Comment.all
  end

  # GET /comments/1
  # GET /comments/1.json
  def show
  end

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

  # GET /comments/1/edit
  def edit
  end

  # POST /comments
  # POST /comments.json
  def create
    @comment = @post.comments.build(comment_params)

    respond_to do |format|
      if @comment.save
        format.html { redirect_to @comment, notice: 'Comment was successfully created.' }
        format.json { render action: 'show', status: :created, location: @comment }
      else
        format.html { render action: 'new' }
        format.json { render json: @comment.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /comments/1
  # PATCH/PUT /comments/1.json
  def update
    respond_to do |format|
      if @comment.update(comment_params)
        format.html { redirect_to @comment, notice: 'Comment was successfully updated.' }
        format.json { head :no_content }
      else
        format.html { render action: 'edit' }
        format.json { render json: @comment.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /comments/1
  # DELETE /comments/1.json
  def destroy
    @comment.destroy
    respond_to do |format|
      format.html { redirect_to comments_url }
      format.json { head :no_content }
    end
  end

  private
    def set_post
      @post = Post.find(params[:post_id])
    end

    # Use callbacks to share common setup or constraints between actions.
    def set_comment
      @comment = @post.comments.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def comment_params
      params.require(:comment).permit(:post_id, :content)
    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>
  <thead>
    <tr>
      <th>Post</th>
      <th>Content</th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>

  <tbody>
    <% @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 %>
  </tbody>
</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

Shallow Nesting

Sometimes it is a better option to use shallow nesting. For our example the config/routes.rb would contain the following routes:
Blog::Application.routes.draw do
  resources :posts do
    resources :comments, only: [:index, :new, :create]
  end

  resources :comments, except: [:index, :new, :create]
end
That would lead to a less messy rake routes output:
$ rake routes
          Prefix Verb   URI Pattern                            Controller#Action
   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
           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
                 PATCH  /posts/:id(.:format)                   posts#update
                 PUT    /posts/:id(.:format)                   posts#update
                 DELETE /posts/:id(.:format)                   posts#destroy
    edit_comment GET    /comments/:id/edit(.:format)           comments#edit
         comment GET    /comments/:id(.:format)                comments#show
                 PATCH  /comments/:id(.:format)                comments#update
                 PUT    /comments/:id(.:format)                comments#update
                 DELETE /comments/:id(.:format)                comments#destroy
$
Shallow nesting trys to combine the best of two worlds. And because it is often used there is a shortcut. You can use the following config/routes.rb to achieve it:
Blog::Application.routes.draw do
  resources :posts do
    resources :comments, shallow: true
  end
end

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.

Thank you for your support and the visibility by linking to this website on Twitter and Facebook. That helps a lot!