A Ten-minute URL Shortener

    Craig Anderson
    Share

    Downstairs from SitePoint, the team at 99designs recently revamped its URL shortener to support a bunch of new features. Using a combination of Sinatra and Heroku, I was flabbergasted at how easy it was.

    What We’re Building

    Before I jump in, let me make it clear: we’re not building a full-blown URL shortener like bit.ly or TinyURL. Instead, we’re going to take a request like http://gentle-light-20.heroku.com/khw (Heroku will give you your own randomly generated, serene-sounding name for development purposes) and redirect the user to another URL. In this example, we’ll send the user to https://www.sitepoint.com/?p=26564, which WordPress will redirect to a friendlier URL.

    I’ve also assumed you have the following installed on your system:

    • Ruby
    • the Gem package management system
    • the RSpec testing framework
    • Git

    If you need help with these, Heroku’s Quickstart Guide has links to help you out.

    A Note on Base 36 Numbers

    In the above example, you might be wondering how we go from khw to 26564. Well, khw is a base 36 number.

    If you’re familiar with hexadecimal color codes, you’ll be familiar with the idea of a base 16 counting system. That’s where digits can not only be between 0 and 9, but can also be A (which corresponds to 10), B (which corresponds to 11), and so on through to F (which has a value of 15).

    A base 36 numbering system extends this idea so that a digit can be anything between 0 and 9 or A to Z. The table below gives some examples of base 36 numbers and their decimal counterparts:

    Base 36 number Decimal value
    9 9
    A 10
    B 11
    Y 34
    Z 35
    10 36
    1BC 47064

    Step 1: Set Up Your Environment

    To start, we’ll need to install some Gems. Gems are Ruby libraries, and can be installed by using the gem command. We’ll need to install the following gems for our application:

    • heroku to administer our application
    • rspec to test our application
    • sinatra to provide the framework for our application
    • rack and rack-test, which provide Ruby with an interface with the web server

    To install these Gems, run the following commands:

    $ sudo gem install heroku
    Successfully installed rake-0.8.7
    Successfully installed mime-types-1.16
    ⋮
    Installing RDoc documentation for json_pure-1.4.6...
    Installing RDoc documentation for heroku-1.10.5...
    $ sudo gem install sinatra
    Successfully installed rack-1.2.1
    Successfully installed sinatra-1.0
    ⋮
    Installing RDoc documentation for rack-1.2.1...
    Installing RDoc documentation for sinatra-1.0...
    $ sudo gem install rspec
    **************************************************
    
    Thank you for installing rspec-1.3.0
    
    Please be sure to read History.rdoc and Upgrade.rdoc
    for useful information about this release.
    
    **************************************************
    Successfully installed rspec-1.3.0
    ⋮
    Could not find main page README.rdoc
    $ sudo gem install rack
    Successfully installed rack-1.2.1
    1 gem installed
    Installing ri documentation for rack-1.2.1...
    Installing RDoc documentation for rack-1.2.1...
    $ sudo gem install rack-test
    Successfully installed rack-test-0.5.4
    1 gem installed
    Installing ri documentation for rack-test-0.5.4...
    Installing RDoc documentation for rack-test-0.5.4...
    $ 

    Step 2: Write Some Tests

    Now that we have our Gems installed, we can start writing some Ruby code. Let’s start with some simple tests. Create a directory called my-url-shortener. Then, inside a file called my-url-shortener-test.rb, put in the following:

    require "my-url-shortener"
    require "spec"
    require "rack/test"
    
    set :environment, :test
    
    describe "My URL Shortener" do
    	include Rack::Test::Methods
    
    	def app
    		Sinatra::Application
    	end
    
    	it "redirects to a blog post" do
    		get "/123" # 123 (base 36) == 1371 (base 10)
    		last_response.status.should == 301
    		last_response.headers["location"].should == "https://www.sitepoint.com/?p=1371"
    	end
    
    	it "redirect to blog post with lowercase digits" do
    		get "/a" # a (base 36) == 10 (base 10)
    		last_response.status.should == 301
    		last_response.headers["location"].should == "https://www.sitepoint.com/?p=10"
    	end
    
    	it "redirect to blog post with uppercase digits" do
    		get "/A" # A (base 36) == 10 (base 10)
    		last_response.status.should == 301
    		last_response.headers["location"].should == "https://www.sitepoint.com/?p=10"
    	end
    
    	it "redirects to home" do
    		get "/"
    		last_response.status.should == 301
    		last_response.headers["location"].should == "https://www.sitepoint.com/"
    	end
    
    	it "unrecognised path redirects" do
    		get "/foo/bar"
    		last_response.status.should == 301
    		last_response.headers["location"].should == "https://www.sitepoint.com/foo/bar"
    	end
    end
    

    In the above code, we’re testing the following redirects:

    Requested path Redirect destination
    /123 https://www.sitepoint.com/?p=1371
    /A https://www.sitepoint.com/?p=10
    /a https://www.sitepoint.com/?p=10
    / https://www.sitepoint.com/
    /foo/bar https://www.sitepoint.com/foo/bar

    Running these tests will only work if there’s a class called my-url-shortener, so let’s create that. Sinatra will do all of the work for us if we just create a file called my-url-shortener.rb with the following line:

    require "sinatra"
    

    Now we can run the test by entering spec my-url-shortener-test.rb at the command line:

    $ spec my-url-shortener-test.rb
    FFFFF
    
    1)
    'My URL Shortener redirects to a blog post' FAILED
    expected: 301,
         got: 404 (using ==)
    ./my-url-shortener-test.rb:16:
    
    2)
    'My URL Shortener redirect to blog post with lowercase digits' FAILED
    expected: 301,
         got: 404 (using ==)
    ./my-url-shortener-test.rb:22:
    
    3)
    'My URL Shortener redirect to blog post with uppercase digits' FAILED
    expected: 301,
         got: 404 (using ==)
    ./my-url-shortener-test.rb:28:
    
    4)
    'My URL Shortener redirects to home' FAILED
    expected: 301,
         got: 404 (using ==)
    ./my-url-shortener-test.rb:34:
    
    5)
    'My URL Shortener unrecognised path redirects' FAILED
    expected: 301,
         got: 404 (using ==)
    ./my-url-shortener-test.rb:40:
    
    Finished in 0.020694 seconds
    
    5 examples, 5 failures

    Here we can see that all five tests have failed, and our redirector just returns 404s. Let’s do something about that, shall we?

    Step 3: Write Your Application

    Add the following to my-url-shortener.rb:

    get %r{^/([0-9a-zA-Z]+)$} do
    	postid = params[:captures][0].to_i(36)
    	redirect "https://www.sitepoint.com/?p=#{postid}", 301
    end
    

    These four lines:

    • intercept any request matching the regular expression ^/([0-9a-zA-Z]+)$ (that is, anything starting with a slash followed by one or more alphanumeric characters),
    • extract a Post ID from the incoming request, converting a base 36 number into an integer,
    • redirect the user to https://www.sitepoint.com/?p=postid, and
    • stop handling this request

    Rerunning the test shows that we’ve made some progress:

    $ spec my-url-shortener-test.rb
    ...FF
    
    1)
    'My URL Shortener redirects to home' FAILED
    expected: 301,
         got: 404 (using ==)
    ./my-url-shortener-test.rb:34:
    
    2)
    'My URL Shortener unrecognised path redirects' FAILED
    expected: 301,
         got: 404 (using ==)
    ./my-url-shortener-test.rb:40:
    
    Finished in 0.0337120000000001 seconds
    
    5 examples, 3 failures

    The three dots in the first line of these results shows us that three of our tests passed. The two remaining cases can be handled by adding a catchall to the end of my-url-shortener.rb:

    get "/*" do
    	path = params["splat"]
    	redirect "https://www.sitepoint.com/#{path}", 301
    end
    

    Let’s try running the tests now:

    $ spec my-url-shortener-test.rb
    .....
    
    Finished in 0.008923 seconds
    
    5 examples, 0 failures

    Success!

    Now, let’s deploy this sucker to Heroku.

    Step 4: Deploy to Heroku

    In order to deploy to Heroku, we’ll need to set up two config files: one called .gems, which contains a list of the Gems our application requires, and another called config.ru, which tells Heroku how to run our app. These files, like the others in this project, are super simple.

    Put this in .gems:

    sinatra

    Put this in config.ru:

    require "my-url-shortener"
    run Sinatra::Application

    Now we’re ready to deploy our application. If you’re yet to do so, visit Heroku and sign up. Once you’ve done this, run through the instructions in Heroku’s Quickstart Guide to create a basic Heroku application.

    In Mac OS X, this boils down to:

    $ cd ~/my-url-shortener/
    $ git init
    Initialized empty Git repository in /Users/craiga/my-url-shortener/.git/
    $ git add .
    $ git commit -m "Creating URL shortener application"
    [master (root-commit) b602ee0] Creating URL shortener application
     4 files changed, 57 insertions(+), 0 deletions(-)
     create mode 100644 .gems
     create mode 100644 config.ru
     create mode 100644 my-url-shortener-test.rb
     create mode 100644 my-url-shortener.rb
    $ heroku create
    Enter your Heroku credentials.
    Email: craiga@sitepoint.com
    Password:
    Uploading ssh public key /Users/craiga/.ssh/id_rsa.pub
    Creating gentle-light-20... done
    Created http://gentle-light-20.heroku.com/ | git@heroku.com:gentle-light-20.git
    Git remote heroku added
    $ git push heroku master
    Counting objects: 6, done.
    Delta compression using up to 4 threads.
    Compressing objects: 100% (5/5), done.
    Writing objects: 100% (6/6), 940 bytes, done.
    Total 6 (delta 0), reused 0 (delta 0)
    
    -----> Heroku receiving push
    -----> Sinatra app detected
    
    -----> Installing gem sinatra from http://rubygems.org
           Successfully installed sinatra-1.0
           1 gem installed
    
           Compiled slug size is 236K
    -----> Launching.... done
           http://gentle-light-20.heroku.com deployed to Heroku
    
    To git@heroku.com:gentle-light-20.git
     * [new branch]      master -> master

    Boom! Your application has been deployed to Heroku. Check it out by visiting the URL Heroku gave you (in the above example, it’s http://gentle-light-20.heroku.com).

    Step 5: There Is No Step Five!

    And that’s that. If you have a short domain you’d like to use, such as the 99d.me domain we use here at 99designs, you can assign it to your website using the tools on Heroku’s site. At the time of writing, you’ll need to enter your credit card number, but there’ll only be a charge if you need lots of database space or better performance.