RoR Nested Routes

Nested routes express parent-child relationships between resources directly in the URL structure. When comments belong to articles, the URL /articles/5/comments communicates this relationship instantly. Nested routes make your app's URL structure logical, readable, and aligned with your data model.

Why Nest Routes

Flat routes (less clear):
  GET /comments          ← which article's comments?
  GET /comments/new      ← for which article?

Nested routes (clear):
  GET /articles/5/comments      ← comments for article 5
  GET /articles/5/comments/new  ← new comment on article 5

Declaring Nested Routes

config/routes.rb

resources :articles do
  resources :comments
end

This creates all seven comment routes nested under articles:

Verb    URL                                         Controller#Action
----------------------------------------------------------------------
GET     /articles/:article_id/comments              comments#index
GET     /articles/:article_id/comments/new          comments#new
POST    /articles/:article_id/comments              comments#create
GET     /articles/:article_id/comments/:id          comments#show
GET     /articles/:article_id/comments/:id/edit     comments#edit
PATCH   /articles/:article_id/comments/:id          comments#update
DELETE  /articles/:article_id/comments/:id          comments#destroy

Nested Route Helpers

article_comments_path(@article)              # /articles/5/comments
new_article_comment_path(@article)           # /articles/5/comments/new
article_comment_path(@article, @comment)     # /articles/5/comments/3
edit_article_comment_path(@article, @comment)# /articles/5/comments/3/edit

The CommentsController

The nested controller always finds the parent first, then scopes the child through it:

app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :set_article
  before_action :set_comment, only: [:show, :edit, :update, :destroy]

  def index
    @comments = @article.comments.order(created_at: :asc)
  end

  def new
    @comment = @article.comments.build
  end

  def create
    @comment = @article.comments.build(comment_params)
    @comment.user = current_user
    if @comment.save
      redirect_to article_path(@article), notice: "Comment added."
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit; end

  def update
    if @comment.update(comment_params)
      redirect_to article_path(@article), notice: "Comment updated."
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @comment.destroy
    redirect_to article_path(@article), notice: "Comment deleted."
  end

  private

  def set_article
    @article = Article.find(params[:article_id])
  end

  def set_comment
    @comment = @article.comments.find(params[:id])
  end

  def comment_params
    params.require(:comment).permit(:body)
  end
end

Nested Form

app/views/comments/new.html.erb

<h1>Add a Comment to: <%= @article.title %></h1>

<%= form_with model: [@article, @comment] do |f| %>
  <%= f.label :body, "Your Comment" %>
  <%= f.text_area :body, rows: 5 %>
  <%= f.submit "Post Comment" %>
<% end %>

Passing [@article, @comment] to form_with tells Rails to post to /articles/:article_id/comments automatically.

Nested Links in Views

app/views/articles/show.html.erb

<h1><%= @article.title %></h1>
<p><%= @article.body %></p>

<h2>Comments (<%= @article.comments.count %>)</h2>

<% @article.comments.each do |comment| %>
  <div>
    <p><%= comment.body %></p>
    <small>By <%= comment.user.name %></small>
    <% if current_user == comment.user %>
      <%= link_to "Edit", edit_article_comment_path(@article, comment) %>
      <%= button_to "Delete", article_comment_path(@article, comment), method: :delete %>
    <% end %>
  </div>
<% end %>

<%= link_to "Add a Comment", new_article_comment_path(@article) %>

Shallow Nesting

Full nesting creates long URLs for member actions that do not need the parent ID:

PATCH /articles/5/comments/3   ← article_id is redundant here
                                   comment id=3 is unique on its own

Use shallow nesting to clean this up:

resources :articles do
  resources :comments, shallow: true
end
Result:
  POST   /articles/:article_id/comments        comments#create
  GET    /articles/:article_id/comments        comments#index
  GET    /articles/:article_id/comments/new    comments#new

  GET    /comments/:id                         comments#show
  GET    /comments/:id/edit                    comments#edit
  PATCH  /comments/:id                         comments#update
  DELETE /comments/:id                         comments#destroy

Multiple Levels of Nesting

Rails supports deeply nested routes, but limit nesting to two levels maximum. Three or more levels create confusing URLs and complicated controllers:

AVOID (three levels):
  /magazines/:magazine_id/articles/:article_id/comments/:id

PREFER (shallow nesting):
  /magazines/:magazine_id/articles      ← index and create
  /articles/:id                         ← show, edit, update, destroy
  /articles/:article_id/comments        ← index and create
  /comments/:id                         ← show, edit, update, destroy

Nested Routes Diagram

Database:
  articles: id=5, title="Rails Guide"
    comments: id=1, article_id=5, body="Great post!"
    comments: id=2, article_id=5, body="Very helpful."

URLs:
  /articles/5              ← show article 5
  /articles/5/comments     ← list comments for article 5
  /articles/5/comments/new ← form to add comment to article 5
  /articles/5/comments/1   ← show comment 1 on article 5

Nested routes make URLs self-documenting. Anyone reading /articles/5/comments/new immediately understands the context — a new comment for article number 5. This clarity benefits both users and developers.

Leave a Comment

Your email address will not be published. Required fields are marked *