ruby 75 lines · 3 tabs

Nested Comments on Articles with Shallow Routing and Scoped Lookups in Rails

Shared by codesnips Aug 2026
3 tabs
Rails.application.routes.draw do
  resources :articles do
    resources :comments, shallow: true, only: %i[index new create show edit update destroy]
  end

  root "articles#index"
end
3 files · ruby Explain with highlit

This snippet shows how a nested resource is exposed in Rails without paying the usual cost of deeply nested URLs. In config/routes.rb, comments is nested under articles but declared with shallow: true, which is the crux of the pattern: routes that need the parent for context (index, new, create) keep the /articles/:article_id/... prefix, while routes that already have a unique comment id (show, edit, update, destroy) collapse to flat /comments/:id paths. The result is short, canonical URLs for individual records and correctly scoped collection routes, without hand-writing two route blocks.

Because shallow routing produces two different parameter shapes, the controller must load records differently depending on the action. In CommentsController, a before_action :set_article runs only for the parent-scoped actions and looks the article up by params[:article_id]. A second before_action :set_comment handles the member actions, and it deliberately scopes the lookup through @article.comments when a parent is present, falling back to a global Comment.find otherwise. Scoping the query through the association is what enforces the tenancy boundary: a request for a comment id that does not belong to the given article raises ActiveRecord::RecordNotFound instead of silently loading another article's data.

The create action builds through @article.comments, so the foreign key is set implicitly and mass-assignment cannot be tricked into reparenting a comment. comment_params uses strong parameters to whitelist only body, keeping article_id server-controlled. The redirect_to [@article, @comment] call leans on polymorphic routing so Rails picks the shallow member path automatically.

In Comment model, a default_scope-free ordering is exposed through an explicit recent scope, and a belongs_to :article, counter_cache: true keeps articles.comments_count accurate for cheap display. The not_found rescue centralizes error handling so both lookup strategies degrade to a 404. This approach is worth reaching for whenever a child resource is always accessed in the context of its parent for listing and creation, but benefits from stable, shareable URLs for individual items. The main pitfall is forgetting to scope member lookups through the parent, which reintroduces the authorization hole shallow routing otherwise sidesteps.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Nested Comments on Articles with Shallow Routing and Scoped Lookups in Rails — share card
Link copied