Build and Deploy a URL Shortener using Django REST Framework and Managed Postgres
Tags: tutorial, webdev, programming, django
Published: 2023-12-12 14:30:15


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](https://www.django-rest-framework.org/) 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](http://bit.ly/) or [tinyurl.com](http://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](https://www.koyeb.com/docs/build-and-deploy/deploy-to-koyeb-button) button below:

[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?name=url-shortener&type=git&repository=koyeb/example-django-rest-framework-url-shortener&branch=main&run_command=gunicorn%20urlshortener.wsgi&env[DATABASE_URL]=REPLACE_ME&env[DISABLE_COLLECTSTATIC]=1&env[DJANGO_ALLOWED_HOSTS]=REPLACE_ME)

**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](https://www.python.org/downloads/) version 3.8 or higher installed on your system.
- A [Koyeb account](https://app.koyeb.com/signup) to deploy your URL shortener
- A [GitHub account](https://github.com/) 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](#create-a-python-virtual-environment)
2. [Install Django and Django REST Framework](#install-django-and-django-rest-framework)
3. [Create a Django Project](#create-a-django-project)
4. [Set Up the Postgres Database](#set-up-the-postgres-database)
5. [Create the Model and the Serializer](#create-the-model-and-the-serializer)
6. [Create All the Endpoints and Methods](#create-all-the-endpoints-and-methods)
7. [Deployment Setup](#deployment-setup)
8. [Deploy to Koyeb](#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:

```bash
mkdir url-project
cd url-project
```

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

```bash
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:

```bash
source myenv/bin/activate
```

On Windows:

```bash
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:

```bash
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:

```py
# 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](https://app.koyeb.com/) 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](https://www.koyeb.com/static/images/tutorials/pgvector-hugging-face/koyeb-postgres-database-connection.png)

### 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:

```bash
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`:

```bash
pip install dj-database-url
```

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

```py
# settings.py
import dj_database_url
```

Locate the DATABASES setting in your `settings.py` file and replace it with the following code:

```py
# 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):**

```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):**

```bash
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".

```py
django-admin startapp url
```

Register the App in `settings.py`:

```py
# settings.py
INSTALLED_APPS = [
    "...",
    "rest_framework",
    "url.apps.UrlConfig",
]
```

Then, open the `models.py` file and define the `URL` model there:

```py
# /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`:

```py
# /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.

```bash
# 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](#Redirecting-to-the-Original-URL)
- [Creating a New Short URL](#Creating-a-New-Short-URL)
- [Retrieving URL Statistics](#Retrieving-URL-Statistics)

### 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:

```python
# /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:

```py
# /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:

```py
# /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:

```py
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:

```python
# /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:

```py
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.

```py
# 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.

```py
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:

```html
<!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 ''.

```py
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:

```bash
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:

```bash
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):**

```bash
pip freeze > requirements.txt
```

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

```shell
pip freeze > requirements.txt
```

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

```powershell
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`

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:

```python
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](https://www.koyeb.com/docs/build-and-deploy/build-from-git) or [Docker-based deployment](https://www.koyeb.com/docs/build-and-deploy/prebuilt-docker-images).

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](https://app.koyeb.com/), 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](https://www.koyeb.com/docs/run-and-scale/domains) 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](https://www.koyeb.com/docs/run-and-scale/domains) 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](https://community.koyeb.com/).