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.
