Sinatra URL Shortener
I have chosen to try to write a tutorial for my first post. I’ve never done this before so definitely leave feedback if you’ve got some suggestions.
I’m going to be showing you one way that you can go about making your own URL shortener using Sinatra, DataMapper, and Bundler. I’m going to assume that you’re at least familiar with what those are. If you’re not, and you want to be, I suggest you step back and follow the links to their respective websites.
Getting Started
To get us started, lets go ahead and create a basic skeleton for our application. This part is pretty standard and should be useful when you’re getting started with any project.
First, create the folders for the basic skeleton of the application:
- models
- views
- public
Inside your main folder, create a file named shortener.rb. This is will be our application’s main file. Start by adding the following code:
# Require Rubygems and Bundler
# Bundler will handle the rest of the gems that we use
require 'rubygems'
require 'bundler'
Bundler.require
# This snippet sets up our database.
# If our database exists, load the data. If not, create it.
DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/database.db")
# This defines what happens when our base URL is accessed
get '/' do
haml :index
end
# Save changes made to our database
DataMapper.finalize
DataMapper::auto_upgrade!
Now we need to tell Bundler which gems we’re going to be using in our application. To do that, create a file named ‘Gemfile’ in your main directory and put the following code inside it:
# This tells Bundler where to get the gems from
source :rubygems
# Require all of the gems that we'll be using.
gem 'sinatra'
gem 'dm-core'
gem 'dm-sqlite-adapter'
gem 'dm-postgres-adapter'
gem 'dm-migrations'
gem 'dm-validations'
gem 'haml'
gem 'rack-flash'
All of the gems that begin with ‘dm’ are for DataMapper, the rest should be self-explanatory. Using bundler is really convenient, especially for deployment. It makes sure that the production environment is using the same gems as your development environment. You can even specify specific versions of gems.
Views with HAML
Remember when we told Sinatra to load a HAML file named ‘index’ when our base URL is accessed? Well, that file doesn’t exist yet, so we should fix that. Create ‘index.haml’ and ‘layout.haml’ in your views folder.
The ‘layout.haml’ file will contain the basic structure of the page that is repeated throughout your app. Mine looks like this:
!!! 5
%html
%head
%meta(charset="utf-8")
%meta(name="author" content="Eric Kelly")
%title Sinatra URL Shortener
%link(rel="stylesheet" href="/css/style.css")
%body
#container
/ Individual page content loads here
= yield
Remember that indentation is really important with HAML. If something is not aligned properly, the resulting HTML won’t be what you want. The ‘= yield’ is where the individual page content is loaded in. In our case, this is where ‘index.haml’ will be loaded in.
In the ‘index.haml’ file, add the following code:
%h1 URL Shortener
%p We've finally got this thing working!
Now we can test everything we’ve done so far to make sure that we didn’t make any errors yet. In order for Bundler to work properly, we need to run ‘bundle install’ from the command line while in our application’s directory. This will install all of the gems that we listed inside ‘Gemfile.’
I use Shotgun to test my application and I recommend it for anybody looking to speed up development. In order to use it you will need to first install the gem.
sudo gem install shotgun
Then you can start the application running at http://localhost:9393 with
shotgun shortener.rb
If everything is working, visit http://localhost:9393 and you should see a normal page with our heading and message.
Class Models
Next step is to create the model for the Link class. This will pretty much define all of the properties that each instance of the Link class will be made of, along with all of its methods.
Create a file named ‘Link.rb’ in your models folder with the following code:
class Link
# Include DataMapper's premade helpers.
include DataMapper::Resource
# Give our Link class a property named 'long_url'
# which is saved as a String of up to 1024 characters
# which is validated by DM to be a URL
property :long_url, String, :length => 1024, :format => :url
# A String that can be used as a key
property :short_url, String, :key => true
end
Now we need to include our Link class into our application. Head back to ‘shortener.rb’ and add in the following code after the snippet that connects our app to our database:
# Require models
Dir.glob("#{Dir.pwd}/models/*.rb") { |m| require "#{m.chomp}" }
This will include the code located in all of the files the we add into our models directory.
Helpers
Next we are going to add a helper that will generate a random 3 character combination that will be unique to each URL that we are shortening. Each of these characters can be a lowercase letter (26), an uppercase letter (26), or a number (10). This means that there are 62 possible choices for each of the three characters, meaning that there will be a possible 238,328 (62 * 62 * 62) unique combinations.
To create the helper, add this code after the snippet the requires our models:
def gen_short_url
# Create an Array of possible characters
chars = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
# Create a random 3 character string from our possible
# set of choices defined above.
tmp = chars[rand(62)] + chars[rand(62)] + chars[rand(62)]
# Until retreiving a Link with this short_url returns
# false, generate a new short_url and try again.
until Link.get(tmp).nil?
tmp = chars[rand(62)] + chars[rand(62)] + chars[rand(62)]
end
# Return our new unique short_url
tmp
end
Next, add another helper that returns the absolute URL of your application:
def base_url
@base_url ||= "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}"
end
What We’ve Got So Far
If you want to test what we have so far, make sure that you are in our project’s root folder in the terminal and type:
irb -r shortener.rb
This will open up an interactive ruby session in which you can play around with our code. If we haven’t messed anything up yet, we should be connected to our database and have the ability to test our Link class.
To verify that our database connection is working properly, type the following code to list all of our Link objects that are stored in the database, which should be none:
Link.all
An empty array should be returned.
To try our ‘gen_short_url’ helper, simply type in:
gen_short_url
A random 3 character string should be returned.
If any of these tests didn’t work, go back and figure out what you did wrong!
Routes
Routes in Sinatra are basically instructions that tell Sinatra what to do depending on the URL that the user visits. We already put one route for ’/’ that is telling Sinatra what to do when our base URL is accessed, which is to load our ‘index.haml’ file. Let’s get started defining the rest of our routes!
In order to create a new instance of our Link class in our database we are going to add a form to our ‘index.haml’ file. Inside the file add the following code:
/ A form that allows us to create a new Link
%form(action="/link" method="post")
/ Create the area to input our long URL
%label(for="url") URL:
%br
%input(type="text" name="url" placeholder="http://")
/ Submit button for form
%input(type="submit" value="Shorten")
Now we need to tell Sinatra what to do when our form is submitted. In order to do this we need to head back to our main application file where we already started defining our routes. Add the following code:
post '/link' do
# Create a new link with the URL submitted by the form
# and generate a new short_url
# NOTE: We used Link.new, so @link isn't saved yet!
@link = Link.new(
:long_url => params[:url],
:short_url => gen_short_url)
# Try to save the new Link.
# If returns true, redirect to page showing Link details.
# If returns false, reload index
if @link.save
status 201
redirect '/link/' + @link.short_url
else
status 400
haml :index
end
end
Now that we are able to add Links to our database, let’s create the ‘link.haml’ template that we just referenced in the last route that we defined.
Just like before, create the ‘link.haml’ file in our views directory. This template is just going to display basic information about each of the Links in our database. Since we are storing our target Link object in an instance variable (@link) in our route, we are able to access it’s properties in our view template. Add the following code to the ‘link.haml’ file:
%h1 Success!
%p Your link has been shortened.
%ul
%li
Long URL:
%a(href="#{@link.long_url}") #{@link.long_url}
%li
Short URL:
%a(href="/#{@link.short_url}") #{base_url}/#{@link.short_url}
Now we need to implement the redirect functionality that is pretty much the main purpose of our URL shortener. To do this, add the following code after our other routes:
get '/:short_url' do
# Get the Link from the DB that has its short_url equal
# to the value of the :short_url param in the URL
@link = Link.get(params[:short_url])
# Redirect to the Link's long_url
redirect "#{@link.long_url}"
end
Play Time
Go ahead and try out your new app! It’s not pretty but it should work.
I’m sure there are many different ways to do this same thing and many ways that I could improve upon the way that I did it. I’m not saying my way is the best, but it works. I’d like to hear from anybody who’s got ideas about how to improve this, so leave some feedback.
I’ll be doing more tutorials on things like this, including how I built this website, so come back sometime.