After getting a request of a customer for short URLs (pointing to way longer URLs of their web application) to print on small labels and embed in QR codes, I thought about using an URL shortening service. Then, I realized that in 2 decades of programming I never wrote an URL shortener myself! It didn’t seem a difficult task - especially since I just needed a handful of features - so I decided to go for it.

In the unlikely case you don’t know what a URL shortener is, it’s a thing which transforms any URL you supply, for example:

https://www.cattlegrid.info/2014/12/12/graphemes-code-points-characters-and-bytes.html

in a shorter version, such as:

http://cattle.grid/MEdr34Es

Short URLs have many uses: shorter to write, easier to remember, easier to embed in small spaces (QR codes, tiny labels, …).

A shortening service must be able to perform at least the following tasks:

  • Get a long URL and return the short version (I implemented a JSON web service for this)
  • Redirect to the long URL when the user browser the short one

To keep thing simple and elegant, I decided to use Mojolicious, a Perl framework, to write this web application. More precisely, I used the simpler Mojolicious::Lite, which allows for the application to be self-contained in a single file.

Let’s start from the beginning:

use Mojolicious::Lite;
use String::Random;
use Mojo::Pg;
use Arthas::Defaults::520;

my $version = '1.0.0';

my $config = {
    db      => {
        host => 'localhost', name => 'cgshorten', user => 'dbuser', pwd => 'dbpass',
    },
    auth    => 'writepass',
};

The three external modules are the framework itself, a library to create random strings (for the short URL name) and the Postgresql interface library for storage. Arthas::Defaults::520 is a convenience library of mine to enable some Modern Perl features: you can use it if you wish, but I recommend you write your own depending on your programming habits.

The configuration is made of the database credentials and of a password which allows the user to create short URLs: this is a simple and silly authentication method, and should be improved in production, but it’s OK for explanatory purposes. If you are making a public service, the authentication could be actually removed, substituting it with some checks to prevent abuse.

We define a couple of helpers:

helper mkerror => sub {
    my ($self, $error) = @_;

    return $self->render('json', {
        status  => 'error',
        error   => $error,
    });
};

helper pg => sub {
    state $pg = Mojo::Pg->new("postgresql://$config->{db}->{user}:$config->{db}->{pwd}\@$config->{db}->{host}/$config->{db}->{name}?PrintError=0&RaiseError=1");
};

mkerror is just some boilerplate code to return errors via JSON. pg connects to the database, where URLs are stored. By the way, here’s the schema of the database (it’s just one table, which doesn’t need much explanation):

cgshorten=> \d links
                       Table "public.links"
  Column   |           Type           |        Modifiers         
-----------+--------------------------+-----------------------------
 icreation | timestamp with time zone | non null default now()
 shortcode | character(8)             | non null
 targeturl | text                     | non null

And here’s the big thing, the JSON web service to creare short URLs:

post '/create' => sub($self) {
    my $args = $self->req->json;
    my $db = $self->pg->db;

    # Handle obvious error cases
    return $self->mkerror('invalid-JSON-content')
        if !defined $args;
    return $self->mkerror('invalid-auth-information')
        if $args->{auth} ne $config->{auth};

    return $self->mkerror('please-provide-URL') if !$args->{targeturl};
    my $targeturl = Mojo::URL->new( $args->{targeturl} );
    return $self->mkerror('invalid-URL') if !defined $targeturl;
    return $self->mkerror('invalid-URL-scheme')
        if $targeturl->scheme ne 'http' && $targeturl->scheme ne 'https';

    my $link = $db->query('select shortcode from links where targeturl = ?', $targeturl)->hash;
    if (defined $link) {
        return $self->render('json', {
          status      => 'ok',
          action      => 'fetched',
          shortcode   => $link->{shortcode},
          shorturl    => $self->url_for("/$link->{shortcode}")->to_abs,
        });
    }

    my $string_gen = String::Random->new;
    my $shortcode = $string_gen->randregex('[A-Za-z0-9]{8}');

    my $saved = 0;
    my $loopcount = 0;
    while ( !$saved ) {
        if ( $loopcount == 10 ) {
            return $self->mkerror('too-much-recursion-in-insert');
        }
        try {
            $db->query('insert into links (shortcode, targeturl) values (?, ?)', $shortcode, $targeturl);
            $saved = 1;
        } catch {
            die $_ if $_ !~ m/duplicate\s+key/;
        };
        $loopcount++;
    }

    return $self->render('json', {
        status      => 'ok',
        action      => 'created',
        shortcode   => $shortcode,
        shorturl    => $self->url_for("/$shortcode")->to_abs,
    });
};

After handling JSON and authentication errors, the target (long) URL gets analyzed to see if it’s valid, otherwise an error is returned.

The software then checks if there is already a shortened version of that URL, in which case it returns the shorturl successfully, with action => fetched, so the client application knows the short URL already existed.

If, on the other hand, no short URL exists for the input data, an 8 characters string is created, in order to obtain a result such as http://cattle.grid/MEdr34Es. The correspondence between the original URL and the random code is saved in the database: this is done using a loop, so the short code is generated again in the (very unlikely) event it already existed and was assigned to a different input URL; the loop is repeated at most 10 times, which is enough unless an extraordinary astral coincidence is in place. :-)

The resulting shorturl is then prepared using the created shortcode and returned to the client in the JSON structure, with an action => created parameter.

The last part of the web application is the redirection service:

get '/:shortcode' => [shortcode => qr/[A-Za-z0-9]{8}/] => sub($self) {
    my $link = $self->pg->db->query('select targeturl from links where shortcode = ?', $self->stash->{shortcode})->hash;
    if (defined $link) {
        $self->res->code(303);
        return $self->redirect_to( $link->{targeturl} );
    }
    return $self->render(text => 'Notfound-shortcode');
};

This is dead simple: the shortcode is looked in the database; if it exists, a 303 redirection to the original (long) URL is performed, otherwise an error is displayed.

That’s it: less than 100 lines of code for an URL shortener!

The full source is available here.

There are a lot of features which could be added:

  • Make short URLs expire/delete after some time (can be done with another 10 lines of code)
  • Count the number of uses of short URLs, maybe logging IPs
  • Add support for more easy-to-read (or even customizable) short codes
  • Provide an home page with a form for shortening URLs directly via web