A Ten-minute URL Shortener
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 applicationrspec
to test our applicationsinatra
to provide the framework for our applicationrack
andrack-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.