James Wilding Freelance Rails, Ruby, and iPhone Web App Developer

Clone TinyURL (and friends) with Rails

tr.im is closing down, and the blogs are a-chatter about why URL-shortening services are probably a bad idea if you care about your links, and why you should roll your own service if you can.

Which is where Rails comes in.


It’s possible to clone TinyURL using Sinatra, but I’m more interested in Rails (I think Rails probably scales better). The basic setup is so simple, it doesn’t take long to write the code, and with Rails you can always add extras like an admin interface later without much fuss.

The code

(If you’re feeling lazy, the entire URL-shortening app is available on Github)

My TinyURL-like Rails app — I call it “Shrink” — has three main components: routes, controller, and model. Routes first:

[ruby]
ActionController::Routing::Routes.draw do |map|

map.resources :locations, :only => [:new, :create]

map.with_options :controller => ‘locations’ do |map|
map.root :action => ‘new’
map.preview ‘/:shrunken/preview’, :action => ‘preview’
map.location ‘/:shrunken’, :action => ‘redirect’
end

end
[/ruby]

“Locations” are the web pages we’ll be redirecting people to when they click on a short URL.

As you can probably tell, this routes file gives us RESTful routes for creating new locations, along with a catch-all shrunken route which we’ll use to intercept our short URLs and redirect them. There’s also a preview route (more on this in a bit), and I’ve mapped the root path (‘/’) to the new action on LocationsController — this will expose the form for shortening URLs on our site’s home page.

The Controller

We only need one controller, with just a few short methods:

[ruby]
class LocationsController < ApplicationController

def new
@location = Location.new
end

def preview
@location = Location.find_by_short_url(params[:shrunken])
end

def redirect
@location = Location.find_by_short_url(params[:shrunken])
redirect_to @location.url
end

def create
@location = Location.new(params[:location])

if @location.save
redirect_to preview_path(@location)
else
flash[:notice] = ‘Invalid URL’
render :action => "new"
end
end

end
[/ruby]

That’s simple enough — the only thing to note is that we’re using Location. find_by_short_url instead of Location.find, so we can use slugs like ‘a1b3c’ instead of database record IDs in our URLs.

The preview action, by the way, just renders a view which shows the user the shortened version of their URL so they can copy and paste it into Twitter (or whatever).

The Model

[ruby]
class Location < ActiveRecord::Base

validates_presence_of :url
before_create :validate_url

class << self
def find_by_short_url(shrunk)
find(shrunk.to_i(36))
end
end

def to_param
id.to_s(36)
end

private
def validate_url
false unless url_valid?(URI.parse(self.url))
end

def url_valid?(url)
url.kind_of?(URI::HTTP) || url.kind_of?(URI::HTTPS)
end

end
[/ruby]

The model implements our find_by_short_url method, implements some validations, and overrides Rails’ default to_param to provide TinyURL-like short URLs. to_param is used in url_for; the method normally returns an object’s database ID, for URLs like /people/1, but here we return a short string like ‘a1c’ instead.

Note the calls to to_s(36) and to_i(36): these convert record IDs into shorter URL slugs, and back again, using base 36 numbering. By using base 36, we can keep our URLs as short as possible: 10000 in base 10 is ‘7ps’ in base 36. Read more about this useful feature of Ruby at ruby-doc.org.

Putting it all together

The code for this app is open-source and is available on Github; download, run script/server, and go to http://0.0.0.0:3000/ and you’ll see it in action. Proof that you can clone TinyURL using Rails in less than an hour!


Comments Add your comments

nate on August 11 2009

nice one. thanks for posting this and the explanation. i’m sort of blundering my way through learning ruby, and it’s helpful to read clear, real world examples.

James on August 11 2009

Thanks nate! Checkout my latest post for some good Ruby learning resources.

↓ Pete on August 11 2009

Great example, I’ll still run with Sinatra though. This feels too lightweight for a Rails stack.

James on August 12 2009

It is lightweight if it does everything you want, but like I said I think Rails scales (in terms of adding new features) better than Sinatra does ;)