A URL shortener is a tool that can take a long URL and make shorter for easy of sharing, especially on social medias. URL shortener also allows to track the way links are accessed and other parameters as well.

There is an abundant number of URL shortening services available around the web and most offer free services for shortened URL’s on public domains. The reason I went ahead and made an internal shorter is for internal reason and there wasn’t a need of advanced tracking and analytics required.

This is a basic URL shorter which makes use of database Index and some basic ASCII character set encoding to get a random short string.

The one functionality lacking in this is the ability to edit the shortened URL, its determined by the database index ID.

File Structure

For this demo I will be talking only about Django app which does the shortening function.


├── shortner
	├── __init__.py 	
	├── admin.py	
	├── app.py
	├── models.py
	├── urls.py
        ├── utls.py
	├── views.py	
Model

This will be a basic model design with original link and a counter to store the number of times it was accessed.


class Links(models.Model):
    link = models.URLField()
    hits = models.IntegerField(default=0)
    # Flag ( In case you need to disabled the shortened Link )
    active = models.BooleanField(default=True)

    # Status
    created = models.DateTimeField(auto_now_add=True)
    time = models.DateTimeField(auto_now=True)
    status = models.BooleanField(default=True, null=False)

Thats all with the model design each entry will get a database index and it can be used as the shortened url. But that isn’t a fancy one which we are used to so the easy way to make it fancy without adding overhead is to encode the database index integer onto a char map and use that mapped string set instead of the sequential number of database index.

Encode

We can encode the database index which is an integer onto a character set to have a fancier string set to share around, since we are just mapping it to a string set it can be easily decode back without much operational over head as well.


import string
import datetime

_char_map = string.ascii_letters + string.digits
# you can use any string set for mapping but make sure to keep it safe, it is required to decode the string back to number.


def encode(id):
        """
        Encode a positive number into string
        """
        # padding year will give you more control over the string set generated but its your choice.
        num = str(datetime.date.today().year) + str(id).zfill(4)
        num = int(num)

        alphabet = _char_map

        if num == 0:
            return alphabet[0]
        arr = []
        base = len(alphabet)
        while num:
            num, rem = divmod(num, base)
            arr.append(alphabet[rem])
        arr.reverse()
        return ''.join(arr)
Decode

To decode the string we need to reverse the process using the same character set to map out the integer and remove calendar year which was appended to the database index.


import string

_char_map = string.ascii_letters + string.digits

def decode(_str):
        """
        Decode the encoded string into the number
        """
        alphabet = _char_map
        base = len(alphabet)
        strlen = len(_str)
        num = 0

        idx = 0
        for char in _str:
            power = (strlen - (idx + 1))
            num += alphabet.index(char) * (base ** power)
            idx += 1

        num = str(num)
        link_id = num[4:] # first 4 digits will be the year refer encoding section
        return int(link_id)

Since these functions encode and decode operated on all objects in the model without any change we can enclose them inside models.py itself.

For ease of use the encode function is defined as model functions and the decode functions is defined as static function to the model, you can use any type depending on your requirement.

View

With most of the shortening functions done in model itself the only tasks left for view is to accept the request and redirect if conditions are met or return appropriate error messages.


from django.shortcuts import redirect, get_object_or_404

from shortlinks.models import Links
from shortlinks.utls import update_request_count_task

def link(request, id):
    db_id = Links.decode_id(id)
    link_db = get_object_or_404(Links, id=db_id, active=True, status=True)
    update_request_count_task.(db_id)
    return redirect(link_db.link)

The update request count is kept in separate function so it can be executed as an async task to cut the time required for update before the user is redirected

Source Code