How to Build, Test and Deploy a URL Shortener with Django

How to Build, Test and Deploy a URL Shortener with Django

Heard of bit.ly and tinyurl? Those are examples of URL Shorteners. Let’s build our own with Django.

Django is a popular high-level Python Web Framework used for creating fast and efficient web applications. With Django, you can focus on building your project without reinventing the wheel. It was initially released in 2005 and has now become one of developers' go-to web frameworks.

In this extensive guide, you will:

  1. Build a web API with Django and Django REST Framework.

  2. Test the integrity of your app using Django’s built-in Test module.

  3. Deploy your web app to the internet with Render – a platform for hosting web applications.

PREREQUISITE

To continue with this guide, you need:

  • Python installed on your computer. Install it here if you don't have it installed.

  • to understand the basics of Git and GitHub. I have written a comprehensive and beginner-friendly guide on Git and GitHub here.

TECHNOLOGIES USED

You would make use of the following technologies to build the URL shortener application:

Backend: Django, Django REST Framework

Frontend: HTML, CSS, JS, JQuery, AJAX (Asynchronous Javascript and XML)

DEMO

This is the outcome of this tutorial. You would be able to build and deploy your version of this application when you are done with this guide.


Let's get right into it...

Create a Project directory

Create a directory named LINK_SHORTENER which will contain your project. To do this on the command line, enter the following command:

mkdir LINK_SHORTENER

Create a virtual environment

A virtual environment helps you isolate your project's installed packages from the other packages on your system. Create and activate a virtual environment named env using the venv tool. venv usually comes pre-installed with Python. On your command line, use the following command to create and activate a virtual environment:

# Create virtual environment
python –m venv env

# Activate the virtual environment
Source env/bin/activate        # Linux or MacOS
env\Scripts\activate.bat    # Windows

Install Django and Django REST Framework

pip install Django
pip install djangorestframework

Create your Django project

Create a Django project named linkify using the django-admin command, and navigate into it:

django-admin startproject linkify
cd linkify

Start the development server

Verify that your project works by running Django’s development server:

python manage.py runserver

Now that the server is running, visit http://127.0.0.1:8000 with your web browser. You’ll see a “Congratulations!” page, with a rocket taking off. It worked!

Create your app

Now that you've got your development server up and running, create an app named core:

python manage.py startapp core

Add your newly created app to the list of Django INSTALLED_APPS:

  1. Open the settings.py file in your linkify directory.

  2. Locate INSTALLED_APPS list and append ‘core’ to the list. Don’t forget to also add ‘rest_framework’ to the list:

INSTALLED_APPS = [
    ...
    'rest_framework',
    'core',
]

Create your Django Model

A model is a Python class that stores information about an object. Create a model called Link that stores information about a link object.

from django.db import models

class Link(models.Model):
    link = models.URLField(max_length=256)
    short_link_id = models.CharField(max_length=256, unique=True)

    def __str__(self):
        return self.short_link_id

    def get_absolute_url(self):
        return reverse('redirect', args=(self.short_link_id,))

The model also contains:

  • link field that stores a long link

  • short_link_id that stores the id of a short link

  • a get_absolute_url method that returns the absolute URL of a short link.

Serializers convert model instances into JSON (JavaScript Object Notation) format and also convert JSON data back to model instances.

Write a serializer class for the Link model:

In the core directory, create a file named serializers.py and add your serializer class in it:

from rest_framework import serializers
from .models import Link

class LinkSerializer(serializers.ModelSerializer):
    absolute_url = serializers.ReadOnlyField(source='get_absolute_url')
    short_link_id = serializers.CharField(allow_blank=True)

    class Meta:
        model = Link
        fields = ['link', 'short_link_id', 'absolute_url']

The absolute_url field in the serializer class is used to hold the data returned by the get_absolute_url method when serializing a model instance to JSON format. The absolute_url field is a ReadOnlyField field because you won’t be storing the field in your model.

The serializer class also modified the short_link_id field and set allow_blank=True because you want it to be optional for users. Our code would generate a random short link if this field is empty.

Write your Views

Create the view that does the actual link shortening and handles the business logic of your app. Use class-based views so you can write short and more concise code.

Open the views.py file in your core app and add the necessary imports:

from django.shortcuts import redirect, render
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Link
from .serializers import LinkSerializer
import random, string

Under the imports above, add the API view that handles the actual link shortening:

class ShortenLink(APIView):
    """
    Create a short url for a given url
    """
    def shorten(self):
        """returns a random 7 character string"""
        return "".join(random.choices(string.ascii_lowercase + string.digits, k=7))

    def post(self, request):
        if not request.data["link"]:
            return Response({"error": "Add a link to shorten"}, status=409)
        short_link = request.data["short_link_id"] if request.data["short_link_id"] else None
        if short_link:
            accepted_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
            if any((char not in accepted_chars) for char in short_link):
                return Response({"error": "Invalid character found in new url"}, status=409)
            try:
                Link.objects.get(short_link_id=short_link)
                return Response("Short link already exists", status=409)
            except Link.DoesNotExist:
                serializer = LinkSerializer(data=request.data)
                if serializer.is_valid():
                    serializer.save()
                    return Response(serializer.data, status=201)
                return Response(serializer.errors, status=400)
        else:
            serializer = LinkSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save(short_link_id=self.shorten())
                print(serializer.validated_data)
                return Response(serializer.data, status=201)
            return Response(serializer.errors, status=400)

Now, let me explain in plain English what exactly this code does:

  1. If the request does not contain a link to be shortened, return an error message.

  2. Store the user’s optional short link in a variable named short_link .

  3. If the optional short link is not empty:

    • Check if the link contains invalid characters. If it does, return an error message. If it doesn’t, validate and serialize the data into our model then save it.
  4. If the preferred short link is empty:

    • Check if the long link is valid. If it is valid, save its short link with a randomly generated string. Else, return an error.

Add a get method to the ShortenLink class. Right above the post() method, add the following:

def get(self, request):
    return render(request, 'index.html')

This method returns an html file when a GET request is made to your API.

Let's also add a view that handles the redirection from a short link to its original link.

In your views.py file add the RedirectLink class:

class RedirectLink(APIView):
    """
    Redirect a short link to its original link
    """
    def get(self, request, id):
        link_object= Link.objects.get(short_link_id=id)
        return redirect(link_object.link)

This view only accepts GET requests

Add URL Routes

Open the urls.py file in linkify project directory (the directory that contains settings.py) and add the following URL routes:

from django.urls import path

urlpatterns = [
    path('', include('core.urls')),
]

Add URL routes for the core app. Create a urls.py file in the core directory and add the following routes:

from django.urls import path
from .views import ShortenLink, RedirectLink

urlpatterns = [
    path('', ShortenLink.as_view(), name='shorten'),
    path('<slug:id>/', RedirectLink.as_view(), name='redirect'),
]

Create HTML Templates

Create a directory named templates in the root directory (the directory that contains manage.py) then create a file named index.html inside it.

At the moment, Django is unaware of the new templates directory. Let's fix that:

  1. Open your project's settings.py file

  2. Locate and update the TEMPLATES variable as follows:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Then, in the newly created index.html file, add the following content:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Linkify - Link shortener</title>

</head>
<body>
    <div class="header">
        <p>Linkify</p>
        <h1>Link Shortner</h1>
    </div>
    <div class="container">
        <div class="content">
            <form id="submit">
                <label for="url">Enter long link</label>
                <input type="url" id="link-input" placeholder="Enter http(s):// link">
                <div>
                    <label for="short_url">New link (optional)</label>
                    <div class="extra">
                        <input type="text" value="{{ request.META.HTTP_HOST }}/" readonly>
                        <input type="text" id="short-link-input" placeholder="New url...">
                    </div>
                </div>
                <input type="submit" value="Shorten">
                <span class="loading">...</span>
            </form>
        </div>
    </div>
    <div class="footer">
        <p>Made with ❤ by <a href="https://www.linkedin.com/in/delight-olagbuji">Delight</a></p>
    </div>


    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</body>
</html>

Add CSS Styles

In the index.html file, create a <style> tag in the <head> section and add the following CSS styles in it:

html {
    background: url('https://img.freepik.com/free-vector/flat-vivid-modern-background_23-2148889928.jpg?w=826&t=st=1690783453~exp=1690784053~hmac=44dfe0ba48002e51906ca9bc0acbaed37571583e6a9a74167a8fc6ec0194b6a1') no-repeat center center fixed;
    -webkit-background-size: cover;
    -moz-background-size: cover;
    -o-background-size: cover;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}
body {
    font-family: Verdana, 'Roboto', sans-serif, Monospace;
}
.header {
    text-align: center;
    color: #fff;
    margin-top: 40px;
}
.header p {
    color: orange;
    font-size: 20px;
}
.container {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 60px;
}
.content {
    position: relative;
    width: 85%;
    max-width: 350px;
    background-color: #fff;
    color: #797979;
    padding: 40px;
    border-radius: 10px;
}
.card {
    position: relative;
    padding: 10px 20px 5px;
    text-align: center;
    color: #fff;
    margin-bottom: 40px;
    border-radius: 10px;
}
.success {
    background-color: rgb(56, 184, 56);
}
.danger {
    background-color: rgb(245, 72, 72);
}
.card .actions {
    display: flex;
}
.card .action:first-child {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}
.card .action:nth-child(2) {
    width: 30%;
    cursor: pointer;
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    background-color: rgb(208, 161, 252);
}
.card .action:focus {
    outline: 4px solid rgb(208, 161, 252);
}
.copied {
    position: absolute;
    top: 30px;
    right: 20px;
    background-color: #000;
    color: #fff;
    padding: 10px;
    display: none;
    font-size: 14px;
}
form input, .card .action {
    width: 100%;
    border: 1px solid #d8d8d8;
    outline: none;
    margin: 15px 0 20px;
    height: 40px;
    border-radius: 5px;
    display: block;
    background-color: #f0f6ff;
    box-sizing: border-box;
    padding-left: 10px;
}
form input:focus:not([type="submit"]):not(:read-only) {
    outline: 4px solid rgb(208, 161, 252);
}
form .extra {
    display: flex;
}
form .extra input:nth-child(1) {
    background-color: #eeeeee;
    width: 50%;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    text-align: center;
    border: 1px solid #d1d0d0;
}
form .extra input:nth-child(2) {
    width:  75%;
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}
form input[type="submit"] {
    width: 100%;
    cursor: pointer;
    margin-bottom: 0;
    background-color: orange;
    border: 1px solid orange;
    color: #fff;
    font-weight: bold;
}
form input[type="submit"]:hover {
    width: 100%;
    cursor: pointer;
    margin-bottom: 0;
    background-color: rgb(235, 152, 0);
    border: 1px solid orange;
    color: #fff;
    font-weight: bold;
}
.loading {
    position: absolute;
    bottom: 62px;
    right: 70px;
    font-size: xx-large;
    color: #fff;
    display: none;
}
.footer {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 30px;
}
.footer p {
    text-align: center;
    color: #fff;
}
.footer a {
    color: #fff;
    text-decoration: underline;
}
@media screen and (max-width: 490px) {
    .content {
        padding-left: 20px;
        padding-right: 20px;
    }
}

Add Javascript

Create a <script> tag just above </body> and add the following scripts in it:

$('#submit').submit(function(e){
    e.preventDefault();
    $('.loading').show()
    $('.card').hide()
    $.ajax({
        type: 'POST',
        url: '/',
        data: {
            link: $('#link-input').val(),
            short_link_id: $('#short-link-input').val(),
        },
        success: function(response) {
            $('.loading').hide()
            $('.content').prepend(`
                <div class="card success">
                    <p>Link shortened successfully</p>
                    <div class="actions">
                        <input class="action" id="link" readonly>
                        <button class="action" id="copy">Copy</button>
                        <span class="copied">Copied</span>
                    </div>
                </div>
            `);
            $('#link').val('{{ request.META.HTTP_HOST }}'+response.absolute_url)
        },
        error: function(response) {
            $('.loading').hide()
            $('.content').prepend(`
                <div class="card danger">
                    <p>${JSON.stringify(response.responseText)
                        .replace(/["{}\[\]\\]/g, '')}
                    </p>
                </div>
            `);
        }
    })
})

$(document).on('click', '#copy', (e)=>{
    var elem = document.querySelector('#link');
    elem.select();
    elem.setSelectionRange(0, 99999);
    document.execCommand("copy");
    $('.copied').show().delay(5000).fadeOut();
})

Restart the Development Server

If the development server is not currently running, start it with the following command:

python manage.py runserver

Go to http://127.0.0.1:8000 in your browser and you should see your app running. Go ahead and try it out!


TESTING

Now that you've built the application, test it with the Django test module.

In the core app directory, create a file named tests.py. This file will include all your test cases.

  1. In tests.py, import the TestCase class from Django's test module:

     from django.test import TestCase
    
  2. Import the Link model and LinkSerializer class:

     from .models import Link
     from .serializers import LinkSerializer
    
  3. Create a class that inherits from TestCase:

     class LinkTestClass(TestCase):
         ...
    
  4. Create a method that will run once just before all your tests begin, with the setUpTestData() class method:

     class LinkTestClass(TestCase):
    
         @classmethod
         def setUpTestData(cls):
             data = {
                 "link": "https://delight.com",
                 "short_link_id": "link1"
             }
             serializer = LinkSerializer(data=data)
             if serializer.is_valid():
                 serializer.save()
    

    This method creates a link object which your tests will subsequent tests will use.

  5. Add the first test to your test class:

     def test_get_link(self):
         link_obj = Link.objects.get(short_link_id="link1")
         self.assertEqual(link_obj.link, "https://delight.com")
    

    This test method gets the link object - which you created in the class method above - and uses the assertEqual method to check if the "original link" of the link object is equal to the expected value, which is https://delight.com.

    The assertEqual method accepts two arguments. If the values of these arguments are not equal, the test fails.

Add two more tests to the test class:

def test_get_short_link(self):
    link_obj = Link.objects.get(short_link_id="link1")
    self.assertEqual(link_obj.short_link_id, "link1")

def test_get_absolute_url(self):
    link_obj = Link.objects.get(short_link_id="link1")
    self.assertEqual(link_obj.get_absolute_url(), "/link1/")

Run the tests

Run the tests you just wrote and see if the application passes them. Use the following command to run tests:

python manage.py test

This should be the output of this command:

Now that you have successfully tested your application, proceed to deploy it to the internet so everyone can use it.

DEPLOY THE APPLICATION

Secure your app

Since everyone on the internet would be able to access your application, you need to secure it as much as possible. Fortunately, Django takes security seriously and therefore provides many security features out of the box.

Let’s make some changes to the settings.py file:

  1. Open your project’s settings.py file.

  2. Locate DEBUG and change its value to False. Debug should never be turned on in production.

  3. Locate SECRET_KEY and configure it to use an environment variable:

    1. At the top of your settings.py file, import Python's os module:

       import os
      
    2. Configure SECRET_KEY to use an environment variable:

       SECRET_KEY = os.environ.get('SECRET_KEY')
      

      Note: Later, you might need to add this environment variable to your local computer to run the development server.

Push your code to GitHub

To push your code to GitHub follow the steps below:

  1. Go to github.com and create a new repository.

  2. Initialize an empty git repository in your project's root directory (linkify - the one containing manage.py). Then run the following commands to push your code to GitHub:

        # Add your new github repo url to your local repo
        git remote add origin [repo-url]
    
       # Add all your files and directories
       git add –A
    
       # Commit your changes
       git commit –m “My first commit”
    
       # Push your code to github
       git push --set-upstream origin master
    

Sign up on Render

For this guide, we would be deploying our application to Render, mainly because it is free to start with and also beginner-friendly.

Follow the steps below to set up your Web service on Render:

  1. Go to render.com and click Get Started to sign up.

  2. Select Web Service, when you are logged in to your dashboard.

  3. Connect your GitHub to your render account, after creating your web service. Click Connect to GitHub and follow the steps to grant Render access to the repository which you want to deploy.

Create a Web Service

After connecting your GitHub repository, you will be taken to a screen where you can enter the deployment details of your web app.

  1. Enter a unique name for your app.

  2. Locate the Build command field and input ./build.sh.

  3. Locate the Start command field and input gunicorn linkify.wsgi:application.

  4. Click the Advanced button.

  5. Click Add Environment Variable.

  6. Input SECRET_KEY in the KEY field and your secret key in its VALUE field. Then click Add.

    You can get the value of your secret key from your settings.py file.

  7. Again, input PYTHON_VERSION as KEY and 3.8.5 as its value. Then click Add.

  8. Scroll down to Auto-Deploy and select No, to have full control over your app deployment.

  9. Click on Create Web Service.

Leave the other fields as they are.

Render will attempt to build your web app but will fail because there is no build.sh file - which we had earlier specified as our build command – and a requirements.txt file.

In your local git repository’s root directory, create a file named build.sh and write the following commands into it:

#!/usr/bin/env bash
# exit on error
set -o errexit

# install project packages
pip install -r requirements.txt

# Make database migrations
python manage.py makemigrations
python manage.py migrate

When Render attempts to build your app, it will run this build.sh file and execute the commands in it.

We also need a requirements.txt file which contains all the packages we installed and used in this project. Create a requirements file using the following command:

pip freeze > requirements.txt

If you are on Windows CMD:

  1. Create a file named requirements.txt

  2. On your terminal run the command pip freeze and paste the output into your newly created requirements.txt file.

Add, commit and push your repository with the following commands:

git add -A
git commit –m “Add build file”
git push

Now that you have updated your git repository with the new changes, you can proceed to deploy your app.

Go to your render dashboard and select Manual Deploy, then click Deploy latest commit.

This time, render will build your app successfully but will not be able to serve it without an application server. You need to install an application server for your app.

Use a package called Gunicorn which is a widely used application server, especially for Django apps. To install it, run the following command on your local terminal:

pip install gunicorn

Update your requirements file with the newly installed package:

pip freeze > requirements.txt

If you are on Windows, as usual, update your requirements.txt file with the output of the pip freeze command.

After making these changes, add, commit and push your changes to GitHub using the commands previously outlined.

Again, go to your Render dashboard and deploy your app with the latest commit.

I promise you, your app is almost ready but render will still run into an error while attempting to serve your web app.

Seeing these errors and fixing them one at a time will help you understand what each command does, hence, enhancing your debugging skills as a Software Engineer. Next time you see errors like these you would know how to fix them.

While attempting to serve your app, render will throw an ALLOWED_HOSTS error. You need to permit Render to serve our web app. To fix this error, add your render domain name to your project's allowed hosts.

In your local repository, open your project settings.py file and add the domain name which Render generated for you to your ALLOWED_HOSTS list:

ALLOWED_HOSTS = ['linkify.onrender.com']

Make sure to use your app's domain name. Do not include http:// or https:// or a trailing slash. You can find your app’s domain name in your dashboard.

After making these changes, add, commit and push your file to GitHub.

Go to your render dashboard and deploy your app with the latest commit.

Congratulations, your service is now Live.

Click your app’s domain name to access your web app.


CONCLUSION

Congratulations on deploying your URL Shortener web application.

In this article, you have been able to:

  1. Build a URL Shortener application with Django.

  2. Test the application with Django's Test module.

  3. Deploy our application to the web.

This article covered various concepts like unit testing, APIs, models, serializers, environment variables, deployment, redirects, error handling, Git/GitHub, etc.

Understanding these concepts will help you build your skills as a Software Engineer. Also, building more projects will help you solidify your understanding of these concepts.

Thanks for reading. Till next time, keep building.

Connect with me on GitHub, Twitter and LinkedIn: