Building an API for website on Django by using REST framework

Clock
24.02.2025
Clock
24.02.2025
Clock
8 minutes
An eye
70
Hearts
1
Connected dots
0
Connected dots
0
Connected dots
0

Introduction

Good day. Using my SearchResultParser project as an example, I will show you how you can make an API for your site and how this API can be used from the client side. I am writing the backend in Django, and the Django REST framework will be responsible for creating and configuring its API.
About the client part of the site (frontend). Specifically, this article will describe the work of the React framework with the Axios library. But this is not so important. Working with an API always involves sending some requests to the server and processing the responses from it. And what it is sent by (Axios) or what it is processed by (React) is not so significant. But for the sake of completeness, I will also provide the client part example.
The process of creating a Django REST API for a website can be described in the following steps:
  1. Create a project and pre-install all the necessary packages, configure it
  2. Create models that we want to use on the client side
  3. Create serializers for them.
  4. Create views (handlers) for them
  5. Connect routers and paths
  6. Write and configure a client to work with the developed API
For objective reasons, I will skip the 1st step. Because the SRP project has already been created and configured, you can see how to configure it and bring it into life, and in the meantime I will describe the next steps.
I will also add that you will need to install and connect the following python modules:
  1. django-cors-headers: to activate the so-called Cross-Origin Resource Sharing (CORS) for communication between the React application and the Django API.
  2. djangorestframework: this is a Django application that allows us to easily build an API for our site.
  3. django: the backend of our site, database management
That's it, connect the newly installed modules in settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Django REST framework
'corsheaders',
'rest_framework',
##
'Backend.apps.BackendConfig',
'Frontend.apps.FrontendConfig',
'Authentication.apps.AuthenticationConfig',
# BY ALLAUTH
'allauth',
'allauth.account',
'allauth.headless',
'allauth.mfa',
'allauth.usersessions',
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# REST framework
'corsheaders.middleware.CorsMiddleware',
# BY ALLAUTH
"allauth.account.middleware.AccountMiddleware",
]
Also, please note that I have a separate Backend application. My entire API will be there. It also needs to be connected, because that's where we'll be writing everything.

Setting up REST API for Django, creating models

So, now my site SearchResultPaser has a system for parsing data from search engines. At the time of writing this article, only Google. Plus the ability to add new search engines and delete old ones. But this site is not limited to this.
I should also have so-called presets, that is, in Django language, the Preset model, which will be available to each registered user to save and customize their favorite search queries.
Here is the full code of the model, using this example we will be working on serialization:
class Preset(models.Model):
class Exports(models.TextChoices):
AsJson = 'As JSON'
AsCsv = 'As CSV'
output = models.CharField(max_length=120, choices=Exports, default=Exports.AsExel)
name = models.CharField(max_length=120)
user = models.ForeignKey(get_user_model(), null=True, on_delete=models.CASCADE)
timeCreated = models.DateTimeField(auto_now=True, auto_created=True)
title = models.BooleanField(default=False)
description = models.BooleanField(default=False)
url = models.BooleanField(default=False)
isVerbose = models.BooleanField(default=False)
queries = models.ManyToManyField(Query)

def __str__(self):
return self.name
As you can see, this model has a ManyToMany relationship (One record in the Preset table can be linked to many records in the DB of Query models) with the Query model. Query is a wrapper for storing user queries. I want to draw attention to the fact that I will only save those queries that are necessary for the preset. In general, there is nothing special in this model. Let's look at the Query model:
class Query(models.Model):
query = models.CharField(max_length=512)
searchEngine = models.ForeignKey(SearchEngine, on_delete=models.CASCADE)

def __str__(self):
return self.query
It is even simpler, there is a text field query and a ForeignKey connection with the following model SearchEngine (One record in the Query table can have a connection with only one record of the SearchEngine model).
And of course the SearchEngine model:
class SearchEngineStatus(models.TextChoices):
Ready = 'Ready'
InDevelopment = 'In development'
NeedsSetup = 'Needs setup'

class SearchEngine(models.Model):
name = models.CharField(max_length=120)
icon = models.ImageField(max_length=120, blank=True, null=True)
status = models.CharField(max_length=120, choices=SearchEngineStatus, default=SearchEngineStatus.NeedsSetup)
config = models.JSONField(max_length=500, blank=True, null=True, default=dict)

def __str__(self):
return self.name
Each entry of this model has a current state. Ready - means that the user can select it on the site and get the corresponding SERP results. InDevelopment - means that the corresponding parser has not yet been written and cannot be used now. And the last status, NeedsSetup, means that the user can use this parser, as in the first case, but it requires preliminary configuration in the user profile.
I also want to note the config field. Each search engine is unique when trying to parse it through the official API. In any case, only I can either add or delete entries to the SearchEngine table.

Setting up REST API for Django, creating serializers

To be able to work with these models on the client side (via JS), we will need Serializers. To do this, import the serializers module from rest_framework and write what we want to serialize:
Oh, and one more thing. You should have a Backend/serializers.py file created. Don't forget to create it and then paste the code below.
from rest_framework import serializers
from .models import Preset, SearchEngine, Query

class PresetSerializer(serializers.ModelSerializer):
class Meta:
model = Preset
fields = ('id', 'output', 'name', 'user', 'title', 'description', 'url', 'isVerbose', 'queries')

class QuerySerializer(serializers.ModelSerializer):
class Meta:
model = Query
fields = ('id', 'query', 'searchEngine')

class SearchEngineSerializer(serializers.ModelSerializer):
class Meta:
model = SearchEngine
fields = ('id', 'name', 'icon', 'status')
There is nothing special about these serializers. We create a class through inheritance. Then we select the model and fields that we want to serialize. Moving on.

Setting up a REST API for Django, creating special views

Now, we would like to see what we are adding, deleting or changing. We need to create views in the Backend/views.py file:
from rest_framework import viewsets
from .serializers import PresetSerializer, QuerySerializer, SearchEngineSerializer
from .models import Preset, Query, SearchEngine

class PresetModelView(viewsets.ModelViewSet):
serializer_class = PresetSerializer
queryset = Preset.objects.all()
def get_queryset(self):
if self.request.user.is_authenticated:
return self.queryset.filter(user=self.request.user)
return Preset.objects.none()

class QueryModelView(viewsets.ModelViewSet):
serializer_class = QuerySerializer
queryset = Query.objects.all()
http_method_names = ('get', 'post')

class SearchEngineModelView(viewsets.ModelViewSet):
serializer_class = SearchEngineSerializer
queryset = SearchEngine.objects.all()
http_method_names = ('get')
Let's go through each of them. PresetModelView, its feature is that we filter all our presets depending on the user who wants to receive them. The feature of SearchEngineModelView is that it can only be viewed, but you cannot add or delete records from the database.

Setting up the REST API for Django, connecting views

And the final touch. To finally see the REST API in action, you need to connect the previously created view classes: QueryModelViews, SearchEngineModelViews and PresetModelView. To do this, connect a new router in the Backend/urls.py file:
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from Backend import views
from django.conf.urls.i18n import i18n_patterns

router = routers.DefaultRouter()
router.register(r'presets', views.PresetModelView, 'preset')
router.register(r'queries', views.QueryModelView, 'query')
router.register(r'se', views.SearchEngineModelView, 'se')

urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls)),
# BY ALLAUTH
path('accounts/', include('allauth.urls')),
path("_allauth/", include("allauth.headless.urls")),
]

urlpatterns += i18n_patterns(
path('', include('Frontend.urls')),
path('', include('Authentication.urls')),
)
That's it, done. Let's see what we got. Go to localhost:8000/api/ you'll see something like this:
As you can see in the picture, we have successfully connected and configured our router. Let's go to any of the links provided, for example localhost:8000/api/se/
Or, for example, let's look at the presets page, which is what this whole thing was all about. So, I have two accounts, dima and some, and each has its own presets. Let's see how filtering by user works:
User some
User dima
As you can see, the first user has only one preset with id=3. And the second has 2 presets with id=1 and 2.

Additional Django REST framework setup

You probably have a question that all this is beautiful, but you just want to get a JSON file on the client side and do what you should. I was itching too, and I found a pretty elegant solution to this problem.
Look, in development mode we will be shown previous pages in pictures, but if we change the mode (i.e. make debug=False in settings.py) we will get responses like this:
I have a special extension in Firefox that makes this beauty. The extension is called, by the way, JSONView and is also available for Chrome.
To achieve this result, simply add the following lines to the end of the settings.py file:
if not DEBUG:
REST_FRAMEWORK = {
# Need to be specified explicitly, otherwise for everyone will be available HTML version of REST framework
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
]
}
We just change the default renderer and what we get is... a JSON file.

Working with the API from the client side

Working with the API from the client side is very simple. You need to make the appropriate AJAX request and process the response. For example, this is how I will get all available records in the SearchEngine model:
axios.get("/api/se/", {
headers: {
"accept": "application/json",
'Content-Type': "application/json",
}
})
.then( resp => {
// Your code
})
.catch( error => {
// Your error code
}
For example, I want to add different status icons for each search engine. To get direct data from the response, we will access a special data object in the response and then the fields we need. And depending on the engine status, we will either add a handler to it or not.
axios.get("/api/se/", {
headers: {
"accept": "application/json",
'Content-Type': "application/json",
}
})
.then( resp => {
const list = resp.data
if (engine_list.length > 0){
engine_list = []
}
axios.get("/api/seue/", {
headers: {
"accept": "application/json",
'Content-Type': "application/json",
}
})
.then( resp2 => {
for (let i = 0; i < resp.data.length; i++){
// Find name of engine
var name = list[i]['name']
var icon = list[i]['icon']
var name_block = ""
var event = ""
var isDisabled = true

const config = resp2.data[0]['config']
var status = config[i]['engine-status']
switch (status){
case 'Ready':
var isDisabled = false
var name_block = (
<div className='flex flex-row gap-1 items-center'>
<img className='w-4 h-4' src={icon}></img>
<div>{name}</div>
</div>
)
var event = (event)=>{
onAddQuery(event, t)
}
break
case 'In development':
var name_block = (
<div className='flex flex-row gap-1 items-center'>
<img className='w-4 h-4' src={icon}></img>
<div className="text-gray-500">{name}</div>
<ReportGmailerrorredIcon sx={{ color: red[500] }} ></ReportGmailerrorredIcon>
<div>{status}</div>
</div>
)
break
case 'Needs setup':
var name_block = (
<div className='flex flex-row gap-1 items-center'>
<img className='w-4 h-4' src={icon}></img>
<div className="text-gray-500">{name}</div>
<ReportGmailerrorredIcon sx={{ color: yellow[500] }} ></ReportGmailerrorredIcon>
<div>{status}</div>
</div>
)
break
}
// Push engines to pop up window, for to be selected later
engine_list.push(
<ListItem>
<ListItemButton data-engine_name={name} disabled={isDisabled} onClick={event}>
<div className="flex flex-col gap-6">
<div className="flex flex-row gap-2 items-center justify-between" >
{name_block}
<div id='toRemoveEngineStuff' className='hidden flex flex-row gap-1 items-center w-fit'>
<IconButton id='onDeleteQuery' className='w-fit'><DeleteForeverIcon/></IconButton>
</div>
</div>
<Divider id='toRemoveEngineStuff' className="hidden w-full self-center" component='div' variant="middle"/>
</div>
</ListItemButton>
</ListItem>
)
}
})
.catch( error => {
Msg(`${error}`)
})
})
.catch( error => {
Msg(`${error}`)
})
Yes, I realize this code block is much more complex, with nested AJAX requests. But this example is perfect for demonstrating how to get data from a response.
And here are the responses from the server:
For localhost:8000/api/se/
For localhost:8000/api/seue/
And how it might look on the website:

Conclusion

So, we have finished setting up our site's API using the Django REST framework. Of course, at first it may seem that all this is somehow confusing and too complicated, but believe me, when your project becomes a little more complicated than a one-page landing page, you will immediately remember about the Django REST framework and that it is not so hard to learn ( •̀ ω •́ )y

Comments

(0)

captcha
Send
It's empty now. Be the first (o゚v゚)ノ

Other

Used termins


Related questions