We use cookies to personalize content and ads, to offer features for social media and to analyze the access to our website. We also share information about your use of our website with our social media, weaving and analytics partners. Our partners may combine this information with other information that you have provided to them or that they have collected as part of your use of the Services. You accept our cookies when you click "Allow cookies" and continue to use this website.

Allow cookies Privacy Policy

Create a URL shortener with Ruby on Rails

You know and probably use URL shortener like Rebrandly, Bitly, TinyURL or Tiny.cc. In this short how-to we implement our URL shortener with Ruby on Rails.

Coding


What's a URL shortener?

There are several services and some platforms providing URL shortener. Sometimes they call it link shortener, URL shrinker, Link compressor, vanity URL creator, but they all work the same: 

  1. You enter a URL and the output is a short URL. 
  2. You can use that short URL for example for social media sharing. 
  3. When users click on the short link they are getting redirected to your origin URL.
  4. You can see stats about clicks on your links.

Some services provide a Custom URL shortener, so that you can register your domain and use that domain to shrink your URLs. Using your domain can create more trust for the ones who see the link.

The benefit of using such a service is that you will be able to track down clicks on links you are sharing on social media. The disadvantage is that you rely on third party services and if you can see the statistics of your clicks, the service will also do. 

That's why we want to implement our Link shortener in Ruby On Rails.

The idea

The main model in our little app is a Link. We want to know these things from the link:

  1. URL -> The url someone entered
  2. Slug -> Our generated slug to create a short URL
  3. Clicked -> A counter for tracking clicks on the link

Adding database migration

Create a new database migration with needed attributes

# db/migrate/20191124150000_create_links.rb
  
class CreateLinks < ActiveRecord::Migration
  def change
    create_table :links do |t|
      t.string :url
      t.string :slug
      t.integer :clicked, default: 0
      t.timestamps null: false
    end
  end
end

Create the model

Create a new model called link.rb:

# app/models/link.rb
  
class Link < ActiveRecord::Base
  validates_presence_of :url
  validates :url, format: URI::regexp(%w[http https])
  validates_uniqueness_of :slug
  validates_length_of :url, within: 3..255, on: :create, message: "too long"
  validates_length_of :slug, within: 3..255, on: :create, message: "too long"
end

In our case we assume that no one has a link with more than 255 characters. But you never know, you can also use type "text" in your migration and change the validates_length_of to match a larger length.

You are now able to create links in your database with:

irb(main):001:0> link = Link.create(url: 'https://www.mylink.com/with-a-large-url', slug: 'shorty')

Adding a route

The idea is to create a single route for your app where you find the link in the DB for a given slug. Add this route to your routes.rb.

# routes.rb
  
Rails.application.routes.draw do
  get '/s/:slug', to: 'links#show', as: :short
end

Let's add a short method in our link class that we can get the route directly from the model:

# in app/models/links.rb
  
def short
  Rails.application.routes.url_helpers.short_url(slug: self.slug)
end

This method makes your life easier in the frontend. Just call .short and you get the short url for that link

irb(main):001:0> link = Link.create(url: "https://www.mylink.com/with-a-large-url", slug: "shorty")
irb(main):002:0> link.short
-> "https://www.mylink.com/s/shorty"

Create Link controller

The previously connected route points to a links_controller with the action show. Let's create the controller:

# app/controllers/link_controller.rb
class LinksController < ApplicationController
  
  def show
    @link = Link.find_by_slug(params[:slug]) 
    render 'errors/404', status: 404 if @link.nil?
    @link.update_attribute(:clicked, @link.clicked + 1)
    redirect_to @link.url
  end
  
end
  

We assume you have an error page under views/errors/404, but you can also send an empty response with status 404 when a link was not found with the given slug. If a link for a slug was found, add +1 to the counter and redirect to the original URL.

Try it in your browser with https://www.mylink.com/s/shorty.

Automatic slug creation

If you don't care about the slug you can also generate it randomly. Add this before validation to your link model:

# in app/models/links.rb
  
before_validation :generate_slug
  
def generate_slug
  self.slug = SecureRandom.uuid[0..5] if self.slug.nil? || self.slug.empty?
  true
end

So when you provide a slug, it tries to use that, otherwise you get a random string.

URL shortener API

To make the usage of your URL shortener easier we create a static method in the Link class to provide a small API.

# in app/models/links.rb
  
def self.shorten(url, slug = '')
  # return short when URL with that slug was created before
  link = Link.where(url: url, slug: slug).first
  return link.short if link 
    
  # create a new
  link = Link.new(url: url, slug: slug)
  return link.short if link.save
    
  # if slug is taken, try to add random characters
  Link.shorten(url, slug + SecureRandom.uuid[0..2])
end

This simple API allows you to do in you code stuff like:

irb(main):001:0> Link.shorten("https://www.mylink.com/with-a-large-url", "shorty")
-> "https://www.mylink.com/s/shorty"
irb(main):001:0> Link.shorten("https://www.mylink.com/another-large-url")
-> "https://www.mylink.com/s/4fg31"


I hope you are now able to handle your link shrinking technique in your ruby on rails app.


I ❤️Rails,

Simon


The full model

# app/models/link.rb
class Link < ActiveRecord::Base  
  
  validates_presence_of :url  
  validates :url, format: URI::regexp(%w[http https])  
  validates_uniqueness_of :slug  
  validates_length_of :url, within: 3..255, on: :create, message: "too long"  
  validates_length_of :slug, within: 3..255, on: :create, message: "too long"
  
  # auto slug generation
  before_validation :generate_slug
  
  def generate_slug
    self.slug = SecureRandom.uuid[0..5] if self.slug.nil? || self.slug.empty?
    true
  end
  
  # fast access to the shortened link
  def short
    Rails.application.routes.url_helpers.short_url(slug: self.slug)
  end
  
  # the API
  def self.shorten(url, slug = '')
    link = Link.where(url: url, slug: slug).first
    return link.short if link  
    
    link = Link.new(url: url, slug: slug)
    return link.short if link.save
    
    Link.shorten(url, slug + SecureRandom.uuid[0..2])
  end
  
end

Share this article:

zauberware logo