How to easily hide numerical IDs in Rails 5

This post will help you hide the numerical IDs in Rails URLs. It’ll turn x.com/posts/2 into x.com/posts/a4Sj8d. You can also adjust the length and characters that it uses to generate these URLs if you’d like.

By default, Rails uses URLs that are pretty transparent: x.com/posts/25 signals that this is the 25th post on the site. Sometimes, you don’t want to show that you only have 25 posts. As Nathan Amick puts it, “Every website has a third user, but that third user doesn’t have to know he’s the third user.”

Some people suggest making the change in PostgreSQL, but that approach always seemed convoluted to me. Rails works better when we don’t try to fight against its defaults. Plus, creating something like a GUID would lead to an ugly URL (x.com/posts/70E2E8DE-500E-4630-B3CB-166131D35C21) and be 9 times bigger in size (36 vs. 4 bytes for an integer). Strings also don’t sort as fast as numbers because they rely on collation rules.

There’s a much easier way: a gem called Hashid by Justin Cypret. It’s an 80-line wrapper for the Hashids project, and it can do all the work from the model. This allows your database to use integers as IDs, but the ID will appear as a hash like x4g59d in the URL. It’s lightweight and fast. Add it to your Gemfile:

gem "hashid-rails", "~> 1.0"

And then run bundle install.

Create a file at config/initializers/hashid.rb, and add the following. Customize config.salt to a word or phrase that you like - it’ll scramble the ID to your project.

Hashid::Rails.configure do |config|
  # The salt to use for generating hashid. Prepended with table name.
  config.salt = "Mark is the best"

  # The minimum length of generated hashids
  config.min_hash_length = 6

  # The alphabet to use for generating hashids
  config.alphabet = "abcdefghijklmnopqrstuvwxyz" \
                    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
                    "1234567890"

  # Whether to override the `find` method
  config.override_find = true

  # Whether to sign hashids to prevent conflicts with regular IDs (see https://github.com/jcypret/hashid-rails/issues/30)
  config.sign_hashids = true
end

Then, in whatever model you’d like a hashed URL, include Hashid::Rails. This is what it would look like inside models/post.rb:

class Post < ApplicationRecord
  include Hashid::Rails
end

Now, start up your server, take a look at your post, and it’ll have a hashed URL!

The only problem is that it can still be accessed via its numerical URL. For instance, although you can access it at x.com/posts/g7j3dV, you can still access it at x.com/posts/2, too. Let’s fix that so people can’t look through your records sequentially.

I recommend turning off config.override_find inside the Hashid initializer that we created earlier. This isn’t necessary, but it helps us avoid shooting ourselves in the foot as we continue developing the application:

# Whether to override the `find` method
config.override_find = false

And inside your controller, use find_by_hashid instead of find:

# posts_controller.rb
# ...

def set_post
  @post = Post.find_by_hashid(params[:id])
end

Now, people can’t look at posts via their ID – they need to access them via hash.

If you enjoyed this post, please make a t-shirt with my face on it.