Taking Things Too Far: REST

I’m going to put up a few posts based on a talk I gave at RailsConf ‘09 in Vegas and RailsWayCon in Berlin. Sorry for the delay in updating but I wanted to deliver the talks before posting here.

There’s a common pattern I see when working on code-reviews with ActionRails or my consulting work. People find a new technique, technology or idea and put it to work in their projects. At first they get a huge benefit, problems get solved quickly and things are good. Driven by this initial success they double down, they put their new tool to work in more areas of their application, they even go back over their old stuff and seek out more pure ways to apply it.

However as time passes they find the benefits aren’t quite what they used to be. Their nice new toy has turned into something which they find gets in the way on a regular basis. Eventually they ‘throw that shit out’. Part of this is just the natural progression of technology, something better comes along and we adopt it. But another part of it is our tendency to over do things. The technology we picked up isn’t shit, the promise we saw was real. But we’ve taken it beyond its intended use, learned the wrong lessons and tied ourselves up as a result.

I’m going to cover a few techniques used in the Rails community which are great, but which turn on you if you take them too far. Starting with RESTful design.

Restful Design

RESTful design really started catching on with Rails 1.2, and by the time 2.0 was released it had become something approaching Canon Law. Everyone who was anyone and building a Rails application, was focussing on resources, CRUD and HTTP. There were two chief benefits of this change.

The first benefit, and the one everyone focussed on, was that you had a relatively straightforward way to add an API to your application. Back in the preREST dark-ages everyone who was designing an API for their application had to make a bunch of decisions about how they wanted to build it.

  • Do you re-use your controllers, or have a separate ApiController?
  • How do you pass arguments around?
  • What should the URLs look like?
  • Perhaps XML-RPC or SOAP is the right way?

With REST you get answers to all those questions, and instead of worrying about that, you just get on with building your Application.

Even for applications without an API, REST gives you some benefits. You avoid discussions about what your controllers and actions should be called, and what your URLs should look like. It also makes it easier for new developers to get up to speed with your project. Almost every rails developer now knows that if you’re looking for the thing which creates posts, you’ll be looking at PostsController#create.

Taking it Further

If we look at a slightly more complicated example, we can see the beginnings of the friction that comes from taking things too far. Take an example of a site which lets people upload photos and write blog posts, and lets users comment on one another’s data. The most common way to approach this design would be:

  map.resources :bookmarks, :has_many => [:comments]
  map.resources :posts,     :has_many => [:comments]

The nice thing about this design is that the URLs will reflect the underlying structure of the data you’re managing. For example the URL for comments on post number 5 will be /posts/5/comments and for bookmark 3 will be /bookmarks/3/comments. However where it starts to get a /little/ annoying is when you want to do something generic to all comments, like providing a ‘mark as spam’ link alongside a comment. Because comments exist solely as a child of the Commentable we can’t generate the URLs without knowing the class and id of that object. So it’s just that little bit more difficult to deal with comments generically (e.g. in an admin interface). This tends to lead to you writing a helper something like this:

  def spam_comment_url(comment)
    case o = comment.commentable
      when Post
        spam_post_comment_url(o, comment)
      when Bookmark
        spam_bookmark_comment_url(o, comment)
      end
    end
  end

Now this is a good indicator that you should probably also have a top-level resource for your comments, and thankfully there’s a feature for this case which gives you a nice pragmatic way out.

  # First define the top-level comment resource
  map.resources :comments, :member => {:spam=>:post}

  # then add a shallow collection under each of the commentables
  map.resources :bookmarks do |bookmarks|
    bookmarks.resources :comments, :shallow=>true
  end

  map.resources :posts do |posts|
    posts.resources :comments, :shallow=>true
  end

Taking it Too Far

Unfortunately people often get started with REST and love the way it simplifies their designs and gives them conventions to follow. They then take their new rose coloured glasses and start making sure everything in their app is “purely RESTful”. Every new design decision must be perfectly RESTful, anything which looks like RPC is instantly purged from the application.

Taking this more extreme approach to the problem of marking comments as spam they’ll say something like:

When you think about it, marking a comment as spam is really creating the SpamScore child resource of the comment with the value of spam set to true

And build something like this, so when they want to mark a comment as spam they ‘only’ have to construct a POST request to the bookmark_comment_spam_score_url of /posts/1/comments/2/spam_score:

  map.resources :bookmarks do |bookmarks|
    bookmarks.resources :comments do |bookmark_comments|
      bookmark_comments.resource :spam_score
    end
  end

While this may be purely restful, it’s much more complicated than the ‘impure’ RPC approach taken above with a simple URL like /comments/1/spam. Plus if you want to get truly pure your URLs should probably be more like this:

map.resources :users do |users|
  users.resources :bookmarks do |bookmarks|
    bookmarks.resources :comments do |bookmark_comments|
      bookmark_comments.resource :spam_score
    end
  end
end

The advice I typically give when I come across a “complex but pure” model like this is to go back to basics and remember why we originally started using REST. Does it help us make an API? Does it make things simpler for new developers to follow? Does it make it easier to work with some of the great plugins out there? If the answer to all those questions is no, you should probably dial back the purity and do the pragmatic thing.