Build Url Shortener API In Django - Redis
Tags: python, django, redis, webdev
Published: 2020-09-21 18:43:39

We will build a simple Url Shortener using django and redis, data will be stored in SQLite and redis will be used as cache storage.

### Setup a virtual environment for this project <a name="/magesh236/setup-python-virtual-environment-2ad8"></a>


[Setup a virtual environment for this project](https://dev.to/magesh236/setup-python-virtual-environment-2ad8), so it will not affect the projects in your machine.

Installing the dependency for the project
```
pip install django
pip install redis
```
above command will install the latest version of the packages

### Setting up the django project
for detailed reference about [django](https://docs.djangoproject.com/en/3.1/intro/) visit the page
Now we can create our project
```
django-admin startproject urlshortener
cd urlshortener
```
To check the app running successfully, run this it will open a rocket page(in terminal you can see unapplied migration, we will see this in migrate)
```
python manage.py runserver
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7v0kv8im7k81cojs3602.png)

The below command will create a app call **short**(where logic will be written) inside the **urlshortener**
```
python manage.py startapp short
```

now we have to add the app short to the settings.py, this will help django to identify the changes in models
```python
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'short',
]
```

##### What is django model?
    You can think model as a table in django, in SQL we use tables to store the data, right, In the same way model is being used. Table schema can be created in model then it can be migrated to the database.

by default django shipped with SQLite DB, you can change it to whatever data source you want to connect
```python
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
```

##### models 
Now add this code in short/models.py
```python
from django.db import models

class URL(models.Model):
    full_url = models.TextField()
    hash = models.TextField(unique=True)

    class Meta:
        db_table = 'url_shortener'
```
in full_url we will hold the given url
in hash will store the unique key
db_table holds the name of the table

Run this
```python
python manage.py makemigrations short
```
By running makemigrations, you’re telling Django that you’ve made some changes to your models, the above command only looks for the changes in short app, to load changes from all the app models just run this
```
 python manage.py makemigrations
```

this will creates migrations files inside the story/migrations directory, after that to move the change to SQLite db run this
```python
python manage.py migrate
```
when you run this command this will apply the admin, session related migration to the database, all the warnings in the terminal will be gone

##### View and Urls
Where we will write the logic

create a urls.py inside short directory, after that include the file in urlshortener/urls.py, just like this
```python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include("short.urls")),
]
```

now lets write a simple HttpResponse view called as home, in short/views.py
```python
from django.http import HttpResponse

def home(request):
    current_site = get_current_site(request)
    return HttpResponse("<h1>Url Shortener</h1>")
```
then link the view with a url
```python
from django.urls import path
from short import views

urlpatterns = [
    path('', views.home, name="Home"),
]
```
open the home url (http://127.0.0.1:8000/) you will get the HttpResponse
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nh1xh4fxu11f8wfg7k9e.png)

project is working fine, now lets write some logic for the shortener
for detailed reference check this [Designing a URL Shortening service like TinyURL
](https://www.educative.io/courses/grokking-the-system-design-interview/m2ygV4E81AR)

this will generate a random key value of length 7 with the combination of uppercase, lowercase and numbers. N can be adjusted as we want
```python
import string, random 
# hash length
N = 7
s = string.ascii_uppercase + string.ascii_lowercase + string.digits
# generate a random string of length 7
url_id = ''.join(random.choices(s, k=N))
```

convert the logic to a function and it will create a new record in table for each new url short request
```python
import string, random 
from short.models import URL

def shortit(long_url):
    # hash length
    N = 7
    s = string.ascii_uppercase + string.ascii_lowercase + string.digits
    # generate a random string of length 7
    url_id = ''.join(random.choices(s, k=N))
    # check the hash id is in
    if not URL.objects.filter(hash=url_id).exists():
        # create a new entry for new one
        create = URL.objects.create(full_url=long_url, hash=url_id)
        return url_id
    else:
        # if hash id already exists create a new hash
        shortit(url)
```

this view function will get the request and use the above function to convert it
```python
from django.http import JsonResponse
from django.contrib.sites.shortcuts import get_current_site
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def short_url(request):
    long_url = request.POST.get("url")
    # get the unique id for long url
    hash = shortit(long_url)
    # get the host url
    current_site = get_current_site(request)
    data = {
        "success": True,
        "id": hash,
        "link": "http://{}/{}".format(current_site, hash),
        "long_url": long_url
    }
    return JsonResponse(data)
```

link this view function to short/urls.py urlpatterns list
```python
   path('shorten', views.short_url, name="ShortUrl"),
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hv2c2xc3d4ztgb6rzs9d.png)

Now lets do the retrieving logic
[redis](https://redis.io/) - in-memory data structure store
we will use redis as a cache storage, I'm using redis in my local system
```python
import redis
# host - running host
# port - the port in which its running
# db - by default 0 is the default db, you can change it
rds = redis.Redis(host='localhost', port=6379, db=0)
```
just run it using **redis-server**, once the redis server is stoped data will be cleared.
![Terminal](https://dev-to-uploads.s3.amazonaws.com/i/enl0cddwdi5yalj2i8v6.png)

```python
from django.shortcuts import redirect

def redirector(request,hash_id=None):
    # get the value from redis key, if value not in return None
    hash_code = rds.get(hash_id)
    if hash_code is not None:
        return redirect(hash_code.decode('ascii'))

    if URL.objects.filter(hash=hash_id).exists():
        url = URL.objects.get(hash=hash_id)
        # set the value in redis for faster access
        rds.set(hash_id,url.full_url)
        # redirect the page to the respective url
        return redirect(url.full_url)
    else:
        # if the give key not in redis and db
        return JsonResponse({"success":False})
```
Note: redis in python 3 always return binary string for that we used decode('ascii')

link the redirector view function in short/urls.py urlpatterns list
```python
    path('<str:hash_id>/', views.redirector, name="Redirector"),
```
hash_id is a URL parameters used to get the value from url

![Giphy](https://media.giphy.com/media/s2qXK8wAvkHTO/giphy.gif
)

![Postman](https://dev-to-uploads.s3.amazonaws.com/i/yvi7465p0fhvpoln8tsk.png)


**DONE RIGHT**, this is the project structure
![Vscode](https://dev-to-uploads.s3.amazonaws.com/i/ei25emj2dn07no1d6f6o.png)


Thanks you, have a great day ahead.🤪😎