DEV Community

Cover image for Build and Deploy a URL Shortener using Django REST Framework and Managed Postgres
alisdairbr for Koyeb

Posted on • Originally published at koyeb.com

Build and Deploy a URL Shortener using Django REST Framework and Managed Postgres

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:

Deploy to Koyeb

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:

  1. Create a Python Virtual Environment
  2. Install Django and Django REST Framework
  3. Create a Django Project
  4. Set Up the Postgres Database
  5. Create the Model and the Serializer
  6. Create All the Endpoints and Methods
  7. Deployment Setup
  8. 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
Enter fullscreen mode Exit fullscreen mode

Next, create a virtual environment using the following command (replace myenv with your preferred environment name):

python -m venv myenv
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

On Windows:

myenv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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',
    # ...
]
Enter fullscreen mode Exit fullscreen mode

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.

Koyeb Database Connection Details

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Open your project's settings.py file again. Import dj_database_url at the top of the file:

# settings.py
import dj_database_url
Enter fullscreen mode Exit fullscreen mode

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,
    ),
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Register the App in settings.py:

# settings.py
INSTALLED_APPS = [
    "...",
    "rest_framework",
    "url.apps.UrlConfig",
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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),
    # ...
]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Add the URL pattern for this view in your app's urls.py file:

urlpatterns = [
    # ...
    path('url/', views.create_short_url),
    # ...
]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Add the URL pattern for this view in your app's urls.py file:

urlpatterns = [
    # ...
    path('url/stats/<str:hash>/', views.get_url_stats),
    # ...
]
Enter fullscreen mode Exit fullscreen mode

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
]
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Command Prompt (CMD - Windows):
In the Windows Command Prompt, you can use the following command to generate requirements.txt:

pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

PowerShell (Windows):
In PowerShell, use the following command to generate requirements.txt:

pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Open your Django project's settings.py file in your code editor.

  2. Locate the ALLOWED_HOSTS setting in the file. By default, it may be commented out or set to an empty list.

  3. 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(",")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Select GitHub as the deployment option.
  2. Choose the repository and branch that contain your application code.
  3. 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
  4. 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 value 1, and DJANGO_ALLOWED_HOSTS with the value <YOUR_APP_NAME>-<YOUR_KOYEB_ORG>.koyeb.app.
  5. 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.
  6. 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.

Top comments (0)