Nest routes through multiple controllers to DRY up your code

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):

  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:

  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
        @posts = Post.all
    # rest of controller ...

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.

Rails testing made simple

Learn to test Rails apps the way I learned, building up tests step-by-step, in Everyday Rails Testing with RSpec. Expanded to include exclusive content and a complete sample Rails application. Learn more »

blog comments powered by Disqus