Rob Race

@rob__race

How to use Hash ID’s in your URL in Ruby on Rails 5

When I am building a Rails app that I expect to be public facing in some sort of capacity, I don’t want to be displaying auto-incrementing, integer based IDs in my URLs. Not only does it visually look better, in my opinion. It is a security enhancement by removing the predictability of record discovery via the URL. Though, as ingrained as auto-incremented integer primary keys are in ActiveRecord, I wouldn’t want to rock the boat too much. Some suggest using UUIDs but I find them to be too long for URLs in my taste. So, I set out on how to use Hashes as IDs in my URL and would like to share that with you.

This tutorial is going to assume you already have existing models, however, you will simply just need to add a `hash_id:string` column to your new models. Additionally, you will need to install the friendly_id gem.

# Gemfile
gem 'friendly_id', '~> 5.1.0'

…and of course

bundle install

Perfect, we have the gem needed to do the hashed ID lookups on our models. Next, we need to include the functionality into our models. I chose to create a model concern that will be able to be included in any model we want the Hashed ID functionality on…

# app/models/concerns/friendlyable.rb
module Friendlyable
extend ActiveSupport::Concern
  included do 
extend ::FriendlyId
before_create :set_hash_id
friendly_id :hash_id
end
  def set_hash_id
hash_id = nil
loop do
hash_id = SecureRandom.urlsafe_base64(9).gsub(/-|_/,('a'..'z').to_a[rand(26)])
break unless self.class.name.constantize.where(:hash_id => hash_id).exists?
end
self.hash_id = hash_id
end
end

Let’s go over that file. The first two lines are standard model concern boilerplate.

included do 
extend ::FriendlyId
before_create :set_hash_id
friendly_id :hash_id
end

The include block is basically allowing you to insert lines into your model file as if you were to put them there manually. Thus, we’re adding FriendlyId functionality, adding a before_create callback, and lastly telling FriendlyId we’re using the hash_id column has the FriendlyId lookup column.

def set_hash_id
hash_id = nil
loop do
hash_id = SecureRandom.urlsafe_base64(9).gsub(/-|_/,('a'..'z').to_a[rand(26)])
break unless self.class.name.constantize.where(:hash_id => hash_id).exists?
end
self.hash_id = hash_id
end

In the second half of the file, we’re defining the method called from the before_create hook. Essentially, we’re creating a loop to create a URL safe hash, and set the hash_id attribute if that hash_id does not collide with any existing records. If it’s successful, we break the loop and set the attribute for ActiveRecord to save.

Now that we have the model concern ready to go, it’s really easy to include it in a model:

class User < ApplicationRecord
include Friendlyable
   ...
end

…and that’s it! Let’s add the migration to finish this off.

class AddHashIdToUsers < ActiveRecord::Migration[5.0]
def up
add_column :users, :hash_id, :string, index: true
User.all.each{|m| m.set_hash_id; m.save}
end
  def down
remove_column :users, :hash_id, :string
end
end

The migration will add the column, make it indexed, and then update any existing records to have a hash_id.

The last piece here will be looking up records via FriendlyId, which is a simple update to any finds in your app:

User.friendly.find(params[:id])

…which will use the primary ‘id’ key or your ‘hash_id’ to look up records. There you have it! From this point forward you can be using URLs like http://localhost:3000/users/90upoijsz in your Rails application.

PS- This concept and many otherw are used in my new book for Building a SaaS Ruby on Rails 5(https://BuildASaaSAppinRails.com) and it’s on presale now.

More by Rob Race

Topics of interest

More Related Stories