How to customize ids in URLs in your Rails app

Rails’ URL helpers allow you to pass in an Active Record object and it will automatically infer the proper URL including the id (e.g. link_to "Profile", @user). How can we get this behavior for custom objects?

Custom objects

Consider a custom Image class. We want URLs to look like /images/cat.gif.

Rails relies on the to_param method to generate identifiers in URLs. If we define it on our custom object it can now be used with link_to just like ActiveRecord objects can.

class Image
  attr_reader :filename

  def to_param
    filename
  end
end
<%= link_to "Details", @image %>
<%# will generate <a href="/images/cat.gif">Details</a> %>

Slugs

Because Rails URL-helpers aren’t hard-coded to use ids but instead rely on to_param it allows us to build some fancier behaviors on top such as replacing identifiers with a prettier “slug”. For example when linking to a tag on a blog, we might want a URL that looks like tags/ruby rather than tags/1.

To do this we can override the to_param method to use the name instead of the id. Keep in mind that you will be doing database lookups using this value so use something deterministic!

class Tag < ApplicationRecord
  def to_param
    name.downcase
  end
end

Now when passing a user object into a link or form helper, it will generate a URL with our nice slug

<%= link_to "More #{@tag.name.titleize} content", @tag %>
<%# will generate <a href="/tags/ruby">More Ruby content</a> %>

This technique is how dedicated slug libraries like FriendlyId work.

ActiveModel compatibility

to_param is part of ActiveModel. You get if for free if you include ActiveModel::Conversion (it defaults toid). You can also just define the method directly on your plain old Ruby objects to make them play nicely with the URL helpers.

Writing your custom Ruby objects to be ActiveModel-compatible, whether or not you include any of the actual modules, allows them to integrate natively with Rails’ controller and view helpers. This gives you an experience that’s just as smooth as working with regular Active Record objects.