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.
Table of contents
- Let's get right into it...
- Create a Project directory
- Create a virtual environment
- Install Django and Django REST Framework
- Create your Django project
- Start the development server
- Create your app
- Create your Django Model
- Create Link Serializer
- Write your Views
- Add URL Routes
- Create HTML Templates
- Add CSS Styles
- Add Javascript
- Restart the Development Server
- TESTING
- DEPLOY THE APPLICATION
- CONCLUSION
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:
Build a web API with Django and Django REST Framework.
Test the integrity of your app using Django’s built-in Test module.
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
:
Open the
settings.py
file in yourlinkify
directory.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 linkshort_link_id
that stores the id of a short linka
get_absolute_url
method that returns the absolute URL of a short link.
Create Link Serializer
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:
If the request does not contain a link to be shortened, return an error message.
Store the user’s optional short link in a variable named
short_link
.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.
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:
Open your project's
settings.py
fileLocate 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.
In
tests.py
, import theTestCase
class from Django's test module:from django.test import TestCase
Import the
Link
model andLinkSerializer
class:from .models import Link from .serializers import LinkSerializer
Create a class that inherits from
TestCase
:class LinkTestClass(TestCase): ...
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.
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 ishttps://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:
Open your project’s
settings.py
file.Locate
DEBUG
and change its value toFalse
. Debug should never be turned on in production.Locate
SECRET_KEY
and configure it to use an environment variable:At the top of your settings.py file, import Python's
os
module:import os
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:
Go to github.com and create a new repository.
Initialize an empty git repository in your project's root directory (
linkify
- the one containingmanage.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:
Go to render.com and click Get Started to sign up.
Select Web Service, when you are logged in to your dashboard.
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.
Enter a unique name for your app.
Locate the Build command field and input
./build.sh
.Locate the Start command field and input
gunicorn linkify.wsgi:application.
Click the Advanced button.
Click Add Environment Variable.
Input
SECRET_KEY
in the KEY field and your secret key in itsVALUE
field. Then click Add.You can get the value of your secret key from your
settings.py
file.Again, input
PYTHON_VERSION
as KEY and3.8.5
as its value. Then click Add.Scroll down to Auto-Deploy and select No, to have full control over your app deployment.
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:
Create a file named
requirements.txt
On your terminal run the command
pip freeze
and paste the output into your newly createdrequirements.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:
Build a URL Shortener application with Django.
Test the application with Django's Test module.
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.