Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

  • Blog Navigation
The Rails identity crisis

Imagine, if you will, that you’re a bookseller. You sell books. Big books, small books, serious books, silly books; if it’s got pages and a cover you’ll sell it. Times being what they are you’ve decided to harness the power of the intertubes to sell your books (a novel idea; ho ho ho). In fact, you’ve decided to build a website, and to expose an API with which your business partners can sell books through their websites. Huzzah.

As it turns out you’re an accomplished Rails developer as well as a thriving bibliophile, so you get to work. Fortunately, you thought ahead and already have information for all of your books in a database. Being well read as you are, you choose to make a RESTful Books resource to show off your books. Any customer can check out a book of their choosing by navigating their browser thusly:

Huzzah again. Sort of.

You’ve heard about this SEO thing, and you hate how ugly that URL is, so you override #to_param on your Book model to return a nice looking slug. Now that URL from above looks like this:

You go about your business, quite pleased with yourself, until you receive a phone call from one of the business partners who use your API; it seems they can no longer look at books through your web service.

Here’s the problem: they’re using ActiveResource to consume your RESTful interface. To get a catalog of books they call Book#find(:all), which executes a books#index request. This returns some XML looking like this:

    <title>Stickwick Stapers</title>
    <author>Farles Wickens</author>
    <title>Karnaby Fudge</title>
    <author>Darles Chickens</author>

Now, if they’re interested in Stickwick Stapers by Farles Wickens they call Book#find(1), which returns a 404 error. Oops, of course it does, you’re not looking up books by their database ID any more, you’re looking them up by their URL slug. Your customer needs to call Book#find(’stickwick-stapers’).

Unfortunately, your book XML doesn’t include the URL slug, so your partners are in a bind. Back to work. You change the #to_xml method for your Book model to return something that looks like this:

  <title>Stickwick Stapers</title>
  <author>Farles Wickens</author>

After all, the consumers of your API aren’t really interested in the database ID; or, they shouldn’t be. All is well again, until you get another phone call. It seems now your partners can no longer purchase books through your service.

You’ve exposed the Purchases resource for your partners who want to buy books. A purchase involves simply POSTing to this resource with the ID of the book you want to buy and a quantity (you handle payment offline using a complicated barter system). The POST body looks like this:


OOPS! ActiveRecord doesn’t expect the URL slug for the book, it wants the database ID.

Well, crap. This is a big problem, and one that has no particularly satisfying solution. Here are the candidates:

  1. Send both the database ID and the URL slug in the API, and try to educate all of your API consumers about when to use one vs. the other. Get ready for some serious customer support time.

  2. Override the #book_id= method in the Purchase model to expect a URL slug for the book. Unfortunately, the web site you developed, at great expense, has all sorts of drop-downs and the like stuffed chock full of book IDs. Changing all of that would be a significant expense, never mind the bugs guaranteed to creep in as developers consistently forget that #book_id= doesn’t actually take an ID.

  3. Write the #book_slug= on the Purchase model, and ask your API uses to start using this method instead. Unfortunately, this means changing the web sites that they have developed, at great expense. You just cost them money, never mind the bugs guaranteed to creep in as developers consistently forget that the method to set the book ID is #book_slug=, not #book_id=.

  4. Stop using those silly slugs and just go back to database IDs. Integers are really quite beautiful, aren’t they?

This little ditty is just an example of a fairly serious problem with Rails:

Sometimes we reference domain objects by their database ID (when creating associations), sometimes we reference domain objects by their URL representation (when finding objects in a controller), but in both cases we call the reference that we use the ID.

ActiveResource is an obvious example of the problem. It expects that the XML it receives for an object will have the <id> attribute, and it uses this attribute to build the URL for that object.

Rails routing codifies the problem with its URL parameter naming convention:

map.resources :books # => /books/:id (show/update/destroy)

Is there any surprise this leads to code that looks like this?

def show
  @book = Book.find_by_id(params[:id])

Or this?

it "should succeed" do
  get :show, :id =>

ActiveRecord, whether by intention or not, further enforces this fallacy with the unfortunate convenience that the default implementation of #to_param is simply id.to_s, and that #find_by_id will accept an integer, or a string, or even a string that starts with an integer[1]. So, oftentimes when a project chooses to start using something other than database IDs for URLs the code has a confusing mishmash of methods that use the two interchangeably. Have fun picking that apart.

So, what to do about it? The Rails conventions are largely set in stone, after all, it’s not likely the names of these references will ever change. But, we can be smarter about how we use them:

  1. Stop using #find_by_id in controllers. After all, you’re more than likely not looking for anything there by the database ID. I like the find_by_param plugin as a nice little helper for this. It gives you the #find_by_param and #find_by_param! methods, which you should use in your controllers. It also gives you methods for easily creating URL slugs, but you don’t need to use those until you want them.

  2. Stop writing broken tests. Every time you pass an ID to a routing parameter in a functional test you’re testing a lie. Your tests will pass with the default ActiveRecord behavior, but if you ever decide to override #to_param (most likely after you’ve written about 700 tests like this), they’ll break. My experiences dealing with just this problem on client projects was no small part of the reason I wrote the Wapcaplet plugin and this Rails patch.

  3. Know what you mean and say what you mean. The fact that Rails got this wrong just means that you have to pay closer attention when referencing anything by ID.

  4. Let me know if you come up with any clever solutions.

[1] Rails will treat any string that starts with a database ID the same as the database ID itself in many cases:

  purchase.book_id = "11-thirty-days-in-the-samarkind-desert-with-the-duchess-of-kent"
  genre.book_ids = ["13-how-to-start-a-fight", "16-blogging-for-dummies"]

This fixes the symptom in many cases, but really just further conflates the IDs. And, if you want something without that ugly integer on the front you’re out of luck.

  • Jan

    I think exposing the resource with url like this

    will resolve the problem. It’s SEOed. And you can pass either slug (which contains the ‘1-‘ prefix, of course :) or id to the action.

  • Szymon Nowak

    URL slug is unique, right? So in fact you could use it as your primary key (using non-integer attributes as primary keys is another problem in Rails :)).

    Also quite recently :primary_key option has been introduced for almost all associations (right now it still doesn’t work with has_many :through), so you could point it to your URL slug, if you don’t want to use URL slug as primary key.

  • Actually, there’s a deeper problem – Rails doesn’t get REST quite right (but it’s ok, it still does a better job than the vast majority of web frameworks out there). If we tended towards better following, say, the discoverability principle, the book record would include the URL to get the book’s details:
    Stickwick Stapers
    Farles Wickens

    Wouldn’t this solve both problems?

    (Not to say that I disagree with you about the issues around conflating params[:id] and database ID – I wrote a plugin a long time ago that made dealing with that in routing easier.)

  • Whoops, wasn’t thinking about the formatting for my example. Second try:
    Stickwick Stapers
    Farles Wickens

  • Adam Milligan

    Ben, a couple things:

    1. Your solution won’t work with ActiveResource (although I like the idea in theory). ARes assumes that the content of the <id> tag will be what it appends to the base resource URL for the show/update/destroy path. So, the content of that tag *has* to be the appropriate URL representation for the object. I’ll happily concede that this is an error on the part of ActiveResource, but I’m not sure that it’s an error that’s fixable at this point.

    2. I completely agree that RESTful resources should include their full href, but this is a bit of a pain to implement correctly. To begin with, ActiveRecord objects shouldn’t create URLs; that’s the responsibility of something that deals with HTTP requests, like the controller. Leaking the URL writer in ActiveRecord objects does cause annoying problems at times, like during testing.
    On the project I’m working on now we inject the href into the XML for each object in the controller, although this ends up generating some annoying duplication at the controller level. We decided on this approach as the lesser of two evils; the problem really is that both approaches are at least somewhat evil.

  • Dan Kubb

    What would be nice is if ActiveResource checked for an href attribute in the root element and used that prior to constructing a URL using the id parameter:

    Stickwick Stapers
    Farles Wickens

    Older versions of ARes should ignore it, as well as clients coded to the older spec.

    I agree with Adam that ActiveRecord models shouldn’t know anything about the urls and routing. I’ve solved this problem in the past using a thin Presenter layer that wraps the instance and has a reference to the controller object so that it can construct XML with embedded URIs.

  • I like Dan’s suggestion, actually.

    I also agree that ActiveRecord models shouldn’t know anything about URLs. My solution for that is to not expose ActiveRecord models directly – in [Athena]( (the woefully-incomplete RESTful web framework I spoke about at Rubyconf last year, and have hacked on intermittently since), you expose *resources*, which may or may not have a persistence layer underlying them. It sounds like Dan’s presenters are another way of solving this.

    From listening to Yehuda, it sounds like many of these issues will be easier to workaround in Rails 3, so there’s that.

  • Adam Milligan

    Dan, regarding the href in the book XML, you have a bit of a chicken-and-egg problem there. You need to have the href for the resource in order to get the XML response from the show action in the first place. You could get the href from the index action and then use *that* to get the book.

    Ben, I completely agree with the idea of separating the concept of the resource from the concept of the persistence layer; would you point me at what Yehuda had to say on this?

  • Adam: AFAIK, Yehuda hasn’t talked about separating resource and persistence – that’s something I was working on outside of the Rails paradigm. His talks about refactoring Rails and the changes/modularity coming in Rails 3, though, have suggested to me that we’ll be able to experiment much more with all of this when it comes out. See, for instance, his [talk at Rails Underground](

  • Kelly Felkins


    I like the overall discussion, but I question the original motivation for the change from id to slug. My guess is that search engines in general handle

    as easily as

    A search engine would be a massive spam magnet if it allowed itself to be fooled by the urls presented by sites, just as they ignore keywords and other site contributed guidance.

    In the past, to avoid indexing database driven sites search engine crawlers intentionally avoided cgi style urls with query strings:

    So if you wanted your site to be indexed, one thing you could do was to make the urls look more like paths, __which would include your original url__.

    I think most search engines today use other techniques to control how deep they spider a site and they easily walk urls with query strings. A [google search for brca1]( had a hit on the first page with this url:

    How friendly is that? As an aside, check out the friendly google search url:

    But, of course, they don’t need to be indexed.

    There may be some value to switch from the original, id only url to the slug version — humans can look at the url and may find useful information in the words…

  • Adam Milligan

    Kelly, I won’t pretend to know enough about search engines or web crawling to debate your points. Everything you say looks logical to my untrained eye. However, I’ve had enough requests for “pretty” URLs, independent of SEO, that I think my concerns are valid even if you don’t take search engines into account.

Share This