Sneaky Abstractions

Subscribe to my Feed, follow me on , recommend me on Working With Rails or see my code on GitHub

Cleaning up your nest: How to nest resources with multiple access points

Posted on April 29, 2007 18:50 Tagged with rails, resources, controllers.

With Rails’ new routes nesting resources is easy. Now, whether you should nest or not depends on a lot of things, and others have written smart things about that. Sometimes though, nesting makes sense, and sometimes resources can have multiple access points. In REST terminology, they’re actually different resources, even though in Rails they use the same model. A classic example is this:

class User < ActiveRecord::Base
  has_many :posts
end

class Category < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :user
  belongs_to :category
end

Here, it makes sense to list posts in a certain category and posts that are written by a certain user in addition to just listing all posts. But how do you represent this in a RESTful manner in Rails? The basic assumption seems to be that a controller maps to a model, but I don’t think that’s the best way to do it. If you have multiple resources that use the same model, how would you know in which context the controller is used? For

PostsController
in this example, how would you limit the posts listed based on a user or a category? Sure, it’s doable, but it can get pretty messy. Instead, I suggest thinking of a controller as a resource access point. That is, controllers map to resources. So, in this example, we would have a separate controller for each of these resources:

#/posts
class PostsController < ApplicationController

  def index
    @posts = Post.find(:all)
  end

end

#/category/:category_id/posts
class CategoryPostsController < ApplicationController
  before_filter :find_category

  def index
    @posts = @category.posts
  end

  private
    def find_category
      @category = Category.find(params[:category_id])
    end
end

#/users/:user_id/posts
class UserPostsController < ApplicationController
  before_filter :find_user

  def index
    @posts = @user.posts
  end

  private
    def find_user
      @user = User.find(params[:user_id])
    end
end

Then we map all the resources in routes.rb, specifying which controller to use for the nested resources. We also add a

:name_prefix
to the nested resources so the names don’t clash with the other resources with the same name.

ActionController::Routing::Routes.draw do |map|
  map.resources :posts
  map.resources :users do |user|
    user.resources :posts, :controller => 'user_posts', :name_prefix => 'user_'
  end
  map.resources :categories do |category|
    category.resources :posts, :controller => 'category_posts', :name_prefix => 'category_'
  end
end

But that’s not DRY

There’s a lot of repetition here, but who cares? The controllers have clearly separated concerns because they represent different resources. For

UserPostsController
you may not want to show a single post for example, but instead, in /user/:user_id/posts, point to /posts/:id. Repeating yourself is much better than getting tangled up in increasingly complex abstractions.

We're sailing into a shit typhoon Randy, we'd better haul in the jib before it gets covered in shit