How I use Nano ID in Rails
Topics
Introduction
When building applications normally we will use database to store the data. Defining the primary key in our database is straightforward, the hard part is what data type for the primary key. Basically, the default primary key is bigint and auto-increment, which means when there is a new record created the primary key will increment 1,2,3, and so on. This is the easiest and simplest way to ensure the primary key is unique, there is another option that you may hear which is using UUIDv4 for the primary key. UUIDv4 (Universally Unique Identifier version 4) is a type of UUID that is randomly generated and 128-bit numbers represented as strings, typically consisting of five groups of hexadecimal digits separated by hyphens. UUIDv4 is generated using random or pseudo-random values and each UUIDv4 is considered unique across systems and time and it is designated to have a low probability of collision. Format UUIDv4 looks like this;
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
- xrepresents a hexadecimal digit (0-9, a-f) that is randomly generated
- 4indicates that is version 4 of UUID (UUIDv4)
- yspecifies certain bits that define the UUID variant
UUIDv4 has become popular in web development and making it suitable for generating unique identifiers in distributed systems.
I have personally used UUIDv4 in several applications and the experience has been great. It may be a good option to use UUIDv4 when you want to expose the primary keys to the end user (e.g URL, API) without providing information about the total amount of data in your table. For example:
{
  "id": "21ba44b5-8ce3-4d07-a303-925c01cd83c3",
  "name": "John Doe",
  "country": "Indonesia"
}And here is when using bigint as a primary key
{
  "id": 201,
  "name": "John Doe",
  "country": "Indonesia"
}As you can see, when using bigint we can guess that John Doe is the 201st user. However, if using UUIDv4 we can't guess it. But if you don't have concerns to expose the id of the users regardless the user can guess it or not, bigint could be your best option.
The problems
There is no problem when using UUIDv4 in terms of the performance of the application. But as a developer, I always try to find other options to improve my knowledge and I may implement it in the future. UUIDv4 is a great choice if you don't want to use bigint for the primary key, but as you already see that the result is pretty long and it is hard to copy the UUIDv4 with just a double click because it is separated by hyphen (-) so you need to select manually if you want to copy the whole id. This might be not a good experience at least from my point of view. Another problem is UUIDv4 is not sortable, it means you need to use different columns to sort your data. You can use a timestamp in your table if you have it, you will have this if using migration tools.
- So, how we solve these problems?
- What are we looking for for the solution?
- What if I still want to use bigintbut I need data that can safely show to the end user?
To answer these questions let's go to the next section where I share what I use for my recent applications.
Nano ID
Introducing Nano ID. Nano ID is a tiny, URL-friendly, and unique string ID generator. Nano ID is a library for generating random IDs that still have probability of duplicate IDs. However, this probability is extremely small and it is based on the rules you defined. Here are the features of Nano ID:
- Small - No dependencies, Size Limit controls the size.
- Safe - It uses hardware random generator and it can safely used in clusters.
- Short IDs - Uses larger alphabet than UUID (A-Za-z0-9_-). We can control it.
- Portable - Nano ID has been ported to many programming languages.
Source: Nano ID
To answer previous questions let's break them down one by one
So, how we solve these problems?
There are always advantages and disadvantages of the solution, however looking for other alternative that close to our requirements is important.
What are we looking for for the solution?
We are looking for a safe, small, and easy to control when generating random IDs where we safely can show to the end user and improve the user experience.
What if I still want to use bigint but I need data that can safely show to the end user?
You come to the right place, Nano ID is another good option where you want to control what the user can see while still maintaining using bigint for the primary key and of course you don't have to worry because bigint is sortable. Just like regular int but has more capacity.
Nano ID in Rails
In this post, we are focusing on the Rails ecosystem where try to implement Nano ID in Rails application. Nano ID has been ported to many programming languages including Ruby, you can check the Nano ID for Ruby here.
We are going to build simple Rails application for the Customer data, we will look Stripe Customers page for the basic inspiration. If you check out the Customer details page in Stripe, you will see the URL is using random string instead of an integer. Here is an example
https://dashboard.stripe.com/test/customers/cus_MYkMUCVOIudoZg
The cus_MYgMUCVOIuqoZg is a random unique string generated for the customer and you will see this string across all systems in Stripe. So, we will implement this kind of strategy in the Rails application.
Note that what Stripe implements might be different, so take this post as a reference.
Building application
Let's start by creating empty rails application by run rails new rails-nanoid -T. The -T means we will skip the tests file as we don't need for now. If you want to can add it later.
Then, move to the application directory cd rails-nanoid and if you use Git, don't forget to commit your changes.
git add .
 
git commit -m "Initial commit"Open the app in your text editor, open Gemfile and add nanoid gem or you can add it from your terminal by running
bundle add nanoidThis command will add nanoid to your Gemfile and download it. If you check Gemfile , now you will see the nanoid has been installed.
gem "nanoid", "~> 2.0"We we build a customer data which includes name and country. We will still using bigint for the primary key and adding a new column called public_id to store the random IDs generated by Nano ID. We will not expose the id, instead we will use public_id for the URL routing. For example, to access the customer details the URL will be customers/cus_MYgMUCVOIuqoZg.
Tired of the endless server configuration headaches? WrappedBy takes care of it all for you. Say goodbye to manual setup and hello to hassle-free deployment. Discover the ease of deploying Ruby apps with WrappedBy and revolutionize your development process. Start deploying with WrappedBy
Create Customer Model
We will use the rails scaffold generator to create customers page, this command will create model, views, and controller for us.
rails g scaffold Customer name:string country:stringAdd public_id field with type string and length is 18. Why 18? Here is the illustration:

- cusis the prefix for the- public_idthat has length 3 characters
- _is the separator that has 1 character
- randomIdfollowed by random ID that has length 14 characters
So the total is 18 characters length for our public_id. We need to specify the length of the field so we don't waste the resources in the database.
Then open up the customers migration, in my case will be db/migrate/20230531051206_create_customers.rb and add public_id field, you can named it whatever you want but for suggestion named it something meaningful and easy to understand. So the complete migration for customers table looks like this:
class CreateCustomers < ActiveRecord::Migration[7.0]
  def change
    create_table :customers do |t|
      t.string :public_id, limit: 18, null: false
      t.string :name
      t.string :country
 
      t.timestamps
    end
    add_index :customers, :public_id, unique: true
  end
endIn this migration, we define public_id with string type with limit 18 and can't be null. Also, we add new unique index to ensure there is no duplicate public_id.
Run the database migration by running rails db:migrate.
If you try to create customer, you will get an error because public_id is empty and we have define that public_id can't be null in the database.
Generate Random ID with Nano ID
It's time to add Nano ID to our code, create a new concern file called public_id_generator.rb
touch app/models/concerns/public_id_generator.rbAnd then add this code
 1# frozen_string_literal: true
 2 
 3module PublicIdGenerator
 4  extend ActiveSupport::Concern
 5 
 6  included do
 7    class_attribute :public_id_prefix
 8    self.public_id_prefix = nil
 9 
10    before_create :set_public_id
11  end
12 
13  PUBLIC_ID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
14  PUBLIC_ID_LENGTH = 14
15  MAX_RETRY = 1000
16 
17  class_methods do
18    def generate_nanoid(alphabet: PUBLIC_ID_ALPHABET, size: PUBLIC_ID_LENGTH, prefix: nil)
19      random_id = Nanoid.generate(size:, alphabet:)
20      prefix.present? ? "#{prefix}_#{random_id}" : random_id
21    end
22  end
23 
24  def set_public_id
25    return if public_id.present?
26 
27    MAX_RETRY.times do
28      self.public_id = generate_public_id
29      return unless self.class.exists?(public_id:)
30    end
31    raise "Failed to generate a unique public id after #{MAX_RETRY} attempts"
32  end
33 
34  def generate_public_id
35    self.class.generate_nanoid(prefix: self.class.public_id_prefix)
36  end
37endExplanation:
- Line 7-8: Define the class attribute called public_id_prefixwhere each model that include thePublicIdGeneratorcan specify the prefix based on the requirements. Examplecusfor Customer model
- Line 10: Add Active Record Callbacks before_createto assignpublic_idfield to the random id generated by Nano ID. So this will only be called before creating a resource.
- Line 13: Define constants about what characters that will be used in the generator, the Nano ID will take this constant to generate random IDs.
- Line 14: Define the maximum length of the random ID.
- Line 15: Define the maximum number the application try when there is a collision. In this case, the app will try maximum 1000 times until found the unique random id.
- Line 18-21: Define class method for generating random id with Nano ID which is the actual code of generating random IDs with Nano ID.
- Line 24-32: In line 10, we add callback to call this method. This method doing a job to check whether the unique id has been used or not and assign it to the public_idfield.
- Line 34-36: Call the generate_nanoidmethod and pass theprefixdefined in the model.
Using PublicIdGenerator
After we create a module for generate the public id, now we include the module in Customer model. Also, define the public_id_prefix for the Customer in this case is cus.
 1class Customer < ApplicationRecord
 +  include PublicIdGenerator 
 3 
 +  self.public_id_prefix = "cus" 
 5endSo, if you want to use public_id in more models, what you have to do is add public_id field and include PublicIdGenerator module and you're ready to go.
Now let's try this out, run the application by rails server, open http://localhost:3000/customers in your browser and try to add a new customer. If success, you will be redirected to the details page, notice that the url is still using default id http://localhost:3000/customers/1. However, if you look at the logs you will see the public_id is generated

you can also check in the rails console to the first customer

As you can see, we have successfully generate random IDs with Nano ID. We have one left todo which is expose the public_id instead of id in the URL.
Expose public_id
Currently, in the URL is using id and we want it to use public_id instead. In Rails, this is very common to change the params id in the URL and it is very straightforward.
Open routes file config/routes.rb and change the customers route.
 -resources :customers 
 +resources :customers, param: :public_id Open the customer model and add override to_param method which Action Pack uses for constructing a URL to this object. In this case, we will use public_id instead of id.
 class Customer < ApplicationRecord
 	# code before...
  
 +  def to_param 
 +    public_id 
 +  end 
 endLastly, open the customers controller app/controllers/customers_controller.rb and update to find the customer by public_id
 def set_customer
 -  @customer = Customer.find(params[:id]) 
 +  @customer = Customer.find_by!(public_id: params[:public_id]) 
 endLet's try again, re-start the server and access the customers page. Click one of the customers you've created and see the URL is changed and now using the public_id. In my case, the first customer will be http://localhost:3000/customers/cus_B0VmfGwUidEvV4.
Congratulations!🎉 You have successfully implementing Nano ID in Rails application, with this approach we still maintain regular id for the primary key which supports unique data and sortable.
Conclusion
Although using the default primary key using bigint is good, we must have to look out for requirements first. Define what the application needs, and consider the pros and cons of using a different approach.
Using randomly generated IDs like Nano ID could be a good alternative, however, as a developer, we must understand what Nano ID really does in our application. Defining the number of characters in the generated IDs is also important, to help with that Nano ID has a Collision Calculator to give us how many years in order to have a 1% probability of collision.

In our case, we have 14 characters and it needs at least 57 thousand years to have 1% probability of collision if we generate 1000 IDs per hour.
Download source code: https://github.com/daily-newer/rails-nanoid
References
- https://github.com/ai/nanoid
- https://zelark.github.io/nano-id-cc/
- https://github.com/radeno/nanoid.rb
- https://planetscale.com/blog/why-we-chose-nanoids-for-planetscales-api
Are you tired of the complexities of deploying Ruby on Rails applications? Discover WrappedBy – the ultimate deployment solution for Ruby enthusiasts. Curious to see how it works? Check out our YouTube video here: