Everyday Rails

Nest routes through multiple controllers to DRY up your code

By Aaron Sumner, July 13, 2010.

Here’s a neat trick I saw Ryan Bates do in a Railscasts episode at some point. I honestly can’t find the episode now for reference, but here’s what he did: Using a single controller method, he generated list views of a given model, filtered based on what was passed into the method.

Why would you want to do this? Say you’re writing a blog (sorry to be trite, but it works well for this example). You’ve got three models in your blogging app: Authors, Categories, and Posts. You want to be able to list Posts in the following ways:

. Listing . Path
Posts by category /categories/3/posts
Posts by author /authors/27/posts
All posts /posts

I know those paths could be more human and search engine friendly by replacing the IDs with a token of some kind; I’ll talk about ways to do that in a future post. Check out FriendlyID or this Railscasts episode if you’re in a hurry.

The models

In order for this to work, the first thing you’ll need to do is make sure your model relationships are in place. So an Author has many Posts, a Category has many Posts (for the sake of simplicity we’ll just assign a Post to a single Category, though this technique will apply to a more complex relationship), and a Post belongs to an Author and belongs to a Category.

The routes

Here’s the first part of the trick—you’re not limited to nesting a route under a single parent route. Rather, you can nest it under as many parents as you need to. You can also leave it un-nested. So what does this mean? We can create the following routes (in Rails 2.3.x style):

config/routes.rb
  map.resources :authors,
    :has_many => [ :posts ]
  
  map.resources :categories,
    :has_many => [ :posts ]
    
  map.resources :posts

which gives you the following routes:

. Listing . Path
Posts by category /categories/:category_id/posts
Posts by author /authors/:author_id/posts
All posts /posts

:category_id and :author_id are now param values that you can access in your controller. Let’s look at that now.

The controller

The second part of the trick happens in posts_controller.rb. What you need to do here is check for which param (if any) was passed into the index method, since that’s what we’ll use to generate a list of posts. Here’s what happens:

app/controllers/posts_controller.rb
  class PostsController < ApplicationController

    def index
      if params[:category_id]
        @posts = Category.find(params[:category_id]).posts
      elsif params[:author_id]
        @posts = Author.find(params[:author_id]).posts
      else
        @posts = Post.all
      end
    end
    
    # rest of controller ...
  end
    

So if group_id is passed (like /categories/3/posts) the controller returns a list of that category’s posts to the index view. If author_id is passed (via /authors/27/posts) we get that author’s posts. If neither is passed (/posts) we get a list of all posts.

A real-world example

I’m working on a project at work that involves tagging items and then sharing those items in one or more groups (similar to categories in the example I made up above). Using the method I just outlined, I can display a list of tags that are used on items shared with a given group, or a list of tags used by an individual user, or a list of all the tags in my system.

What do you think? Follow along on on Mastodon, Facebook, or Bluesky to let me know what you think and catch my latest posts. Better yet, subscribe to my newsletter for updates from Everyday Rails, book picks, and other thoughts and ideas that didn't quite fit here.
Buy Me A Coffee

Test with confidence!

If you liked my series on practical advice for adding reliable tests to your Rails apps, check out the expanded ebook version. Lots of additional, exclusive content and a complete sample Rails application.

Newsletter

Ruby on Rails news and tips, and other ideas and surprises from Aaron at Everyday Rails. Delivered to your inbox on no particular set schedule.