Build and Deploy a URL Shortener using Django REST Framework and Managed Postgres
alisdairbr
Posted on December 12, 2023
A URL shortener is a service that transforms long and complex URLs into short, easily memorable ones. This tutorial guides you through creating a URL shortener using Django REST framework and Postgres, deploying it on Koyeb.
Learn to generate short URLs, replace links, and track access counts. This tracking feature is valuable for assessing the performance of marketing campaigns.
By the end, you'll have the skills to develop your URL shortening service, similar to popular platforms like bit.ly or tinyurl.com, and take it live using Koyeb's platform.
Whether you're a Django enthusiast or just looking to build a practical web service, this tutorial will provide you with the steps and insight to kickstart your project.
Want to see the final product in action? You can deploy and preview the URL Shortener application from this guide using the Deploy to Koyeb button below:
Note: Make sure to replace the DATABASE_URL
with your value and ?sslmode=require
added at the end and DJANGO_ALLOWED_HOSTS
with the value <YOUR_APP_NAME>-<YOUR_KOYEB_ORG>.koyeb.app
.
Requirements
Before we dive into building our URL shortener with Django REST and Postgres, ensure that you have all the necessary tools and software in place. Here are the requirements for this project:
- Python version 3.8 or higher installed on your system.
- A Koyeb account to deploy your URL shortener
- A GitHub account to store your Django project code and trigger deployments
Steps
To successfully develop this application and deploy it to production, you will need to follow these steps:
- Create a Python Virtual Environment
- Install Django and Django REST Framework
- Create a Django Project
- Set Up the Postgres Database
- Create the Model and the Serializer
- Create All the Endpoints and Methods
- Deployment Setup
- Deploy to Koyeb
Create a Python Virtual Environment
First, we need to set up a local Django project environment.
This step involves creating a dedicated virtual environment to manage project-specific dependencies and installing the required packages.
To begin, open your terminal and navigate to the directory where you want to create your Django project:
mkdir url-project
cd url-project
Next, create a virtual environment using the following command (replace myenv
with your preferred environment name):
python -m venv myenv
This command will create a virtual environment named myenv
in your project directory.
Activate the Virtual Environment
Next, you'll need to activate the virtual environment. Depending on your operating system, use the appropriate command:
On macOS and Linux:
source myenv/bin/activate
On Windows:
myenv\Scripts\activate
Once activated, you'll notice that your command prompt changes, indicating that you are now working within the virtual environment. This ensures that any packages you install are isolated from your system-wide Python installation.
Install Django and Django REST Framework
With the virtual environment active, we can now install Django and Django REST Framework. Run the following commands:
pip install django
pip install djangorestframework
These commands will install the latest versions of Django and Django REST Framework within your virtual environment.
Now that we have our local Django project environment set up, the next step is to configure Django REST Framework (DRF) and configure our project to use it.
Create a Django Project
Run the following command to create a new Django project:
django-admin startproject urlshortener
This command will create a new directory named "urlshortener" in your project directory, and it will contain the initial project structure. Here's what the resulting folder structure will look like:
your_project_directory/
├── urlshortener/ # Django project folder
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py # Project settings file
│ ├── urls.py # Project-level URL routing
│ └── wsgi.py
└── manage.py # Django's command-line management utility
Configure Django Rest Framework and Set Up the Project
DRF is a powerful toolkit for building Web APIs on top of Django, and it will be essential for creating our URL shortener application.
To use DRF in your Django project, you need to configure it in your project's settings.py
file.
Open your project's settings.py
file, which is typically located in your project's main directory.
Locate the INSTALLED_APPS
list in the settings.py
file. Add rest_framework
to the list of installed apps. It should look like this:
# settings.py
INSTALLED_APPS = [
# ...
'rest_framework',
# ...
]
Set Up the Postgres Database
In this step, we'll set up the Postgres database that our URL shortener application will use. In this tutorial, we will be leveraging Koyeb's Managed Postgres.
To create a Managed Postgres database, access your Koyeb control panel and navigate to the Databases tab. Next, click on the Create Database Service button. Here, you can either provide a custom name for your database or stick with the default one, choose your preferred region, specify a default role, or keep the default value, and finally, click the Create Database Service button to establish your Postgres database service.
Once you've created the Postgres database service, a list of your existing database services will be displayed. From there, select the newly created database service, copy the database connection string, and securely store it for future use.
Install Postgres Drivers
Next, you'll need to install Python drivers to connect to the database. The most common Python library for interacting with PostgreSQL is psycopg2
. You can install it using pip:
pip install psycopg2
This library enables Django to communicate with the PostgreSQL database.
Configure the Database
To configure the database for your Django project, we'll use the dj_database_url
library. This library allows you to specify your database configuration using a URL format, making it easy to switch between different database providers.
Install dj_database_url
using pip
:
pip install dj-database-url
Open your project's settings.py
file again. Import dj_database_url
at the top of the file:
# settings.py
import dj_database_url
Locate the DATABASES setting in your settings.py
file and replace it with the following code:
# settings.py
import dj_database_url
# Use the DATABASE_URL environment variable to configure the database.
DATABASES = {
'default': dj_database_url.config(
conn_max_age=600,
conn_health_checks=True,
),
}
Set the DATABASE_URL
environment variable locally
This configuration tells Django to use the DATABASE_URL
environment variable to determine the database settings.
Linux and macOS (Bash):
export DATABASE_URL="your_database_url_here"
Replace "your_database_url_here" with the actual URL or connection string for your Postgres database.
Windows (Command Prompt):
set DATABASE_URL="your_database_url_here"
Again, replace "your_database_url_here" with the actual URL or connection string for your Postgres database.
This will set the DATABASE_URL
variable only for the active terminal session, it would need to be set again in any other terminal sessions.
Create the Model and the Serializer
Now that we've set up our Django project and database, it's time to define the core components of our URL shortener: the model and serializer. These components will allow us to manage URLs, track visits, and interact with our API.
Define the URL Model
We'll start by defining the URL
model, which represents the URLs we want to shorten. The model will have three fields:
-
hash
(string): A unique identifier for the shortened URL. -
url
(string): The original URL that users want to shorten. -
visits
(integer): A counter to keep track of the number of times the shortened URL has been visited.
At the root of your Django project, create a new app url
for the model and serializer. Reminder: Django "apps" are subsections of functionality in a Django "project".
django-admin startapp url
Register the App in settings.py
:
# settings.py
INSTALLED_APPS = [
"...",
"rest_framework",
"url.apps.UrlConfig",
]
Then, open the models.py
file and define the URL
model there:
# /url/models.py
from django.db import models
class URL(models.Model):
hash = models.CharField(max_length=10, unique=True)
url = models.URLField()
visits = models.PositiveIntegerField(default=0)
def __str__(self):
return self.url
In this model, we use a CharField
for the hash field, a URLField
for the URL field, and a PositiveIntegerField
for the visits field. The hash field is unique to ensure that each shortened URL is unique.
Create the URL Serializer
Next, create a serializer for the URL model. The serializer defines how the URL model should be serialized into JSON format and vice versa. This is essential for interacting with our API.
In your Django app, create a serializers.py
file. This is where you will define the URLSerializer
:
# /url/serializers.py
from rest_framework import serializers
from .models import URL
class URLSerializer(serializers.ModelSerializer):
class Meta:
model = URL
fields = ['hash', 'url', 'visits']
Here, we create a URLSerializer
class that inherits from serializers.ModelSerializer
. We specify the model as a URL and list the fields we want to include in the serialized representation.
Migrate the database
Run the following commands to migrate your database.
# create a migration for your url model
python manage.py makemigrations
# run the migration against the database
python manage.py migrate
Create All the Endpoints and Methods
In this step, we will create the API endpoints for our URL shortener application and implement the necessary methods behind each endpoint. These endpoints will allow users to shorten URLs, retrieve statistics, and access the original URLs via short hashes.
Redirecting to the Original URL
Endpoint: /url/:hash
This endpoint will handle requests to access the original URL associated with a given hash. We will also increment the visits
count for the URL.
Implementation:
In your views.py
file, create a view to handle the redirection there:
# /url/views.py
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
from .models import URL
def redirect_original_url(request, hash):
try:
url = URL.objects.get(hash=hash)
url.visits += 1 # Increment visits count
url.save()
return redirect(url.url)
except URL.DoesNotExist:
return HttpResponseNotFound("Short URL not found")
Next, create url/urls.py
and add the URL pattern to map requests to this view:
# /url/urls.py
from django.urls import path
from . import views
urlpatterns = [
# ...
path('url/<str:hash>/', views.redirect_original_url),
# ...
]
Creating a New Short URL
Endpoint: /url
This endpoint will allow users to submit a long URL and receive a shortened URL in response.
Implementation:
In your views.py
file, create a view to handle URL creation:
# /url/views.py
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
from .models import URL
from rest_framework.decorators import api_view
from django.http import JsonResponse
import hashlib
# (... previous function code ...)
@api_view(['POST'])
def create_short_url(request):
if 'url' in request.data:
original_url = request.data['url']
# Generate a unique hash for the URL
hash_value = hashlib.md5(original_url.encode()).hexdigest()[:10]
# Create a new URL object in the database
url = URL.objects.create(hash=hash_value, url=original_url)
# Return the shortened URL in the response
return JsonResponse({'short_url': f'/url/{hash_value}/'}, status=201)
return JsonResponse({'error': 'Invalid request data'}, status=400)
Add the URL pattern for this view in your app's urls.py
file:
urlpatterns = [
# ...
path('url/', views.create_short_url),
# ...
]
Retrieving URL Statistics
Endpoint: /url/stats/:hash
This endpoint will provide statistics about a specific shortened URL, including the number of visits.
Implementation:
In your views.py
file, create a view to retrieve URL statistics:
# /url/views.py
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
from .models import URL
from rest_framework.decorators import api_view
from django.http import JsonResponse
import hashlib
from .serializers import URLSerializer
from rest_framework.response import Response
def redirect_original_url(request, hash):
## ...
@api_view(['POST'])
def create_short_url(request):
## ...
@api_view(['GET'])
def get_url_stats(request, hash):
try:
url = URL.objects.get(hash=hash)
serializer = URLSerializer(url)
return Response(serializer.data)
except URL.DoesNotExist:
return Response({'error': 'Short URL not found'}, status=404)
Add the URL pattern for this view in your app's urls.py
file:
urlpatterns = [
# ...
path('url/stats/<str:hash>/', views.get_url_stats),
# ...
]
Connecting the URLs
Finally, don't forget to include your app's URLs in your project's urls.py
file. In the project's urls.py
, include your app's URLs using the include function:
Note: This is the urls.py
in your project folder urlshortener
not the urls.py
in the app folder url
. This code connects our app-level URLs to our project-level URLs.
# urlshortener/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('url.urls')), # Include your app's URLs here
]
Create the Home Page for the URL Shortener
We have built a functioning API that can be used on its own or used as part of frontend APPs built-in frameworks like React, Svelte, Solid, Qwik, Angular or Vue. In this step, we will add a small UI using Django's built-in templating features.
This UI will:
- Allow you to create new shortened URLs
- Display existing URLs with details
- Display errors if any error occurs in creating a URL like an integrity error if shortening the same URL multiple times.
To start, add a view function to the url/views.py
file that will pull our URLs from the database and send them to a template which will also have a form to generate our URLs.
def simple_ui(request):
## Get all urls
urls = URL.objects.all()
## Render template
return render(request, "index.html", {"urls": urls})
Create a url/templates
folder and in that folder create an index.html
with the following for our UI:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My URL Shortener</title>
</head>
<body>
<header>
<h1>My URL Shortener</h1>
</header>
<main>
<!-- FORM FOR SHORTENING URLS -->
<form>
<input type="text" name="url" placeholder="URL to Shorten" style="font-size: 16px; padding: 8px;" />
<button>Shorten URL</button>
</form>
<div class="error"></div>
<!-- DISPLAY OF EXISTING URLS -->
<div class="urls">
<!-- JINJA USED TO LOOP OVER URLS SENT TO TEMPLATE BY VIEW FUNCTION -->
{% for url in urls %}
<div class="url">
<a href="/url/{{url.hash}}">
<div class="url-item">Hash: {{url.hash}}</div>
</a>
<div class="url-item">Short URL: /url/{{url.hash}}</div>
<div class="url-item">Points To: {{url.url}}</div>
<div class="url-item">Uses: {{url.visits}}</div>
</div>
{% endfor %}
</div>
</main>
<!-- JAVASCRIPT FOR FORM FUNCTIONALITY -->
<script>
// grab form node from DOM
const form = document.querySelector('form')
// add submit event on form
form.addEventListener('submit', (event) => {
// prevent immediate refresh
event.preventDefault()
// generate form data object
const formData = new FormData(form)
// generate object to send to API endpoint
const requestBody = { url: formData.get('url') }
// make post request to API, don't forget the trailing slash!
fetch('/url/', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})
// if all goes well
.then((response) => {
console.log(response)
if (response.status >= 400) {
return response.text()
}
// refresh page
location.reload()
})
// if something goes wrong
.then((error) => {
// get error string from html error from django
const regex = /<pre[^>]*>(.*?)<\/pre>/s
const match = regex.exec(error)
if (match) {
const innerText = match[1]
alert(innerText)
} else {
alert('No Error Details')
}
})
})
</script>
<!-- CSS STYLING FOR AESTHETICS -->
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #333;
font-family: 'Arial', sans-serif;
color: #fff;
}
header {
width: 100%;
padding: 10px;
box-sizing: border-box;
text-align: center;
margin-bottom: 20px;
}
header h1 {
color: #fff;
margin-bottom: 5px;
}
main {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 600px;
/* Adjusted max-width */
}
form {
display: flex;
flex-direction: column;
align-items: center;
background-color: #444;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border-radius: 8px;
width: 100%;
margin-bottom: 20px;
}
input {
width: calc(100% - 20px);
padding: 10px;
margin-bottom: 10px;
font-size: 16px;
border: 1px solid #666;
border-radius: 4px;
background-color: #555;
color: #fff;
}
button {
background-color: #8cfcc4;
color: #333;
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #64e0a4;
}
.error {
color: red;
margin-top: 10px;
}
.urls {
background-color: #444;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border-radius: 8px;
padding: 20px;
}
.url {
margin-bottom: 10px;
padding: 10px;
background-color: #555;
border-radius: 4px;
display: flex;
flex-direction: column;
}
a {
text-decoration: none;
color: #8cfcc4;
}
a:hover {
text-decoration: underline;
}
.url-item {
margin-bottom: 5px;
}
</style>
</body>
</html>
In the main project urls.py
, import our view and attach it to the main page URL ''.
from django.contrib import admin
from django.urls import path, include
from url.views import simple_ui
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('url.urls')), # Include your app's URLs here
path('', simple_ui)
]
Test the App Locally
Before deploying your URL shortener to production, it's essential to thoroughly test it locally to ensure that all the endpoints and functionality work as expected. In this step, we'll cover how to test your app on your local development environment.
To start testing your app, make sure your Django development server is up and running. Open your terminal, navigate to your project's root directory, and run the following command:
python manage.py runserver
This command will start the development server at http://localhost:8000
, and you should see output indicating that the server is running.
Deployment Setup
Before deploying your Django application, you need to set up your deployment environment. This includes installing Gunicorn, a WSGI HTTP server for serving your application, and generating a requirements.txt
file to specify the dependencies for your project.
Installing Gunicorn
Gunicorn (short for Green Unicorn) is a popular WSGI HTTP server for running Python web applications. It's a recommended choice for deploying Django applications due to its stability and performance.
You can install Gunicorn using the following command, assuming you have Python and pip installed:
pip install gunicorn
Generating requirements.txt
The requirements.txt
file lists all the Python packages and their versions that your Django application depends on. This file is essential for setting up the same environment in production as you have locally. Here's how to generate a requirements.txt file in different command-line environments:
Run the following command in your project's root directory to generate requirements.txt:
Bash (Mac, Linux):
pip freeze > requirements.txt
Command Prompt (CMD - Windows):
In the Windows Command Prompt, you can use the following command to generate requirements.txt:
pip freeze > requirements.txt
PowerShell (Windows):
In PowerShell, use the following command to generate requirements.txt:
pip freeze > requirements.txt
This command will create a requirements.txt
file containing a list of installed packages and their versions. It's important to keep this file up-to-date as you add or update dependencies in your Django project. When deploying to Koyeb or other platforms, you can use this file to ensure that the correct dependencies are installed in your production environment.
With Gunicorn installed and your requirements.txt file generated, you're ready to proceed with the deployment of your Django application on Koyeb or your chosen hosting platform.
Specify correct version in runtime.txt
Make sure to specify the right version of Python by creating a runtime.txt file with the following command:
echo "python-3.11.2" > runtime.txt
Allowed Hosts Configuration
To ensure the security and functionality of your Django application, you need to specify the allowed hosts that are permitted to access your application. This is achieved by configuring the ALLOWED_HOSTS
setting in your Django project's settings.py
file. Additionally, you should set the environment variable for DJANGO_ALLOWED_HOSTS
during deployment, including your Koyeb URL.
Updating settings.py
Open your Django project's
settings.py
file in your code editor.Locate the
ALLOWED_HOSTS
setting in the file. By default, it may be commented out or set to an empty list.Update the
ALLOWED_HOSTS
setting to dynamically read from an environment variable, allowing you to define the allowed hosts during deployment. Add the following code snippet:
import os
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,[::1]").split(",")
This code sets ALLOWED_HOSTS
to the value of the DJANGO_ALLOWED_HOSTS
environment variable if it is defined. If the environment variable is not set, it falls back to a default list of allowed hosts.
Deploy to Koyeb
Now that you have tested your Django application locally and it's working as expected, it's time to deploy it to production using Koyeb. Koyeb offers flexible deployment options, allowing you to choose between two methods: git-based deployment or Docker-based deployment.
In this tutorial, we will be using the git-driven deployment method. GitHub-based deployment is a convenient way to deploy your Django application to Koyeb.
Create a GitHub Repository to host your Django application's code, called URL-Shortener
. Push your code to your remote repository:
git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin [Your GitHub repository URL]
git push -u origin main
You should now have all your local code in your remote repository.
Within the Koyeb control panel, while on the Overview tab, initiate the app creation and deployment process by clicking Create App. On the App deployment page:
- Select GitHub as the deployment option.
- Choose the repository and branch that contain your application code.
- Click Build and deploy settings to configure your Run command by selecting Override and adding the same command as when you ran the application locally,
gunicorn urlshortener.wsgi
- Set environment variables as needed. The variables that should be set include:
DATABASE_URL
with?sslmode=require
added at the end,DISABLE_COLLECTSTATIC
with the value1
, andDJANGO_ALLOWED_HOSTS
with the value<YOUR_APP_NAME>-<YOUR_KOYEB_ORG>.koyeb.app
. - Name your application. For example,
url-shortener
. Keep in mind that this name will be used to create the public URL for your application. You'll want to add a shorter custom domain for the base URL. - Click the Deploy button.
Koyeb will automatically build and deploy your Django application based on changes detected in your GitHub repository.
Once the deployment is complete, you can access your Django REST application by clicking the provided URL ending with .koyeb.app
.
Test the application
Enter a URL in the form and click the Shorten URL button. You should see the endpoint for the original URL in the list. Your application's URL combined with this newly generated endpoint is the new shortened URL. Again, you'll want to configure a shorter, custom domain for this application to make URLs shorter to share.
Conclusion
With your Django application successfully deployed to Koyeb, it becomes accessible to users on the Internet. You can monitor its performance, scale it as needed, and continue enhancing and expanding your web application.
Deploying this application to Koyeb enables you to harness the power of high-performance microVMs, ensuring that your application runs smoothly in the regions where your users are located.
In this guide, we leveraged Koyeb's git-driven deployments. Docker-based deployment offers greater control over your application's environment and dependencies. Docker-based deployments are possible too by adding a Dockerfile to your project's root directory. Choose the deployment method that best aligns with your infrastructure needs and preferences.
Enjoyed this tutorial or have a suggestion to improve it? Share your thoughts with us over on the Koyeb Community.
Posted on December 12, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 12, 2023