Создаём API для сайта на Django c использованием REST framework

Часы
24.02.2025
Часы
24.02.2025
Часы
7 минут
Глазик
106
Сердечки
0
Соединённые точки
1
Соединённые точки
0
Соединённые точки
0

Вступление

Доброго времени суток. На примере моего проекта SearchResultParser я покажу как можно сделать API для вашего сайта и то, как этот API можно будет использовать со стороны клиента. Бэкенд я пишу на Django, а за создание и настройку его API будет заниматься Django REST framework.
О клиентской части сайта (фронтенд). Конкретно эта статья будет описывать работу React фреймворка с Axios библиотекой. Но это, в сущности, неважно. Работа с API всегда представляет собой отправку некоторых запросов на сервер и обработкой обратных ответов. А то чем оно отправляется (Axios) или то, чем оно обрабатывается (React) не столь важно. Но для полноты картины я ещё предоставлю и клиентскую часть.
Процесс создания Django REST API для сайта можно описать следующими шагами:
  1. Создаём проект и предварительно устанавливаем все необходимые пакеты, настраиваем его
  2. Создаём модели которые, хотим использовать на стороне клиента
  3. Создаём сериализаторы к ним.
  4. Создаём представления(обработчики) для них
  5. Подключаем роутеры и пути
  6. Пишем и настраиваем клиент для работы с API
По объективным причинам 1 пункт я пропущу, ибо проект SRP уже создан и настроен, можешь посмотреть как его настроить и привести в рабочий вид, а я тем временем опишу дальнейшие шаги.
Добавлю ещё то, что ты должен будешь установить и подключить следующие python-модули:
  1. django-cors-headers: для активации так называемого Cross-Origin Resource Sharing (CORS) для общения React приложения и Django API.
  2. djangorestframework: это Django приложение, которое позволит нам с лёгкостью построить АПИ для нашего сайта.
  3. django: бэкенд нашего сайта, управление базой данных
Вот так, подключи ново установленные модули в 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",
]
Так же, прошу заметить я имею отдельное приложение Backend. Там будет весь мой API. Его тоже нужно подключить, ибо там мы и будем всё писать.

Настраиваем REST API для Django, создаём модели

Итак, сейчас мой сайт SearchResultPaser имеет систему парсинга данных из поисковых систем. На момент написания данной статьи, только Google. Плюс возможность добавлять новые поисковые системы и удалять старые. Но, данный сайт не ограничивается этим.
У меня ещё должны быть, так называемые пресеты, то есть, говоря на языке джанго, модель Preset, которая будет доступна каждому зарегистрированному пользователю для сохранения и настройки их любимых поисковых запросов.
Вот полный код модели, на её примере мы и будем заниматься сереализацией:
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
Как можно видеть, у данной модели есть связь ManyToMany (Одна запись в таблицу Preset может быть связана со множеством записей в БД, модели Query) с моделью Query. Query - это обёртка для хранения запросов пользователей. Хочу обратить внимание, я буду сохранять только те запросы, которые необходимы для персета. В общем-то, ничего особенного в данной модели больше нет. Расмотрим уже модель Query:
class Query(models.Model):
query = models.CharField(max_length=512)
searchEngine = models.ForeignKey(SearchEngine, on_delete=models.CASCADE)

def __str__(self):
return self.query
Она ещё проще, есть текстовое поле query и связь ForeignKey со следующей моделью SearchEngine(Одна запись в таблицу Query, может иметь связь только с одной записью модели SearchEngine).
Ну и конечно модель SearchEngine:
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
У каждой записи данной модели есть текущее состояние. Ready - значит, пользователь может его выбрать на сайте и получить соответствующий результат парсинга. InDevelopment - значит соответствующий парсер ещё не написан и его нельзя использовать сейчас. И последний статус, NeedsSetup, означает, что пользователь может использовать данный парсер, как и в первом случае, но предварительно требуется настройка в профиле пользователя.
Так же хочу отметить поле config. Каждая поисковая система уникальна при попытке её парсинга, через официальный API. В любом случае добавлять и удалять записи в таблицу SearchEngine, могу и должен только я.

Настраиваем REST API для Django, создаём сериализаторы

Чтобы иметь возможность работать с данными моделями на стороне клиента (через JS), нам потребуются Сереализаторы. Для этого импортируем модуль serializers из rest_framework и пишем, то что хотим сериализовать:
Да, ещё. У тебя должен быть создан файл Backend/serializers.py. Не забудь создать его, а потом вставить код ниже.
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')
Ничего особенного в этих сериализаторах нет. Создаём класс посредством наследования. Потом выбираем модель и поля, которые хотим сериализовать. Двигаемся дальше.

Настраиваем REST API для Django, создаём особые представления

Теперь, нам бы видеть что мы добавляем, удаляем или изменяем. Нужно создать представления в файле Backend/views.py:
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')
Пройдёмся по каждому из них. PresetModelView, его особенность в том что мы фильтруем все наши пресеты в зависимости от пользователя, который хочет их получить. Особенностью SearchEngineModelView, является, то что её можно только просматривать, но не добавлять или удалять записи из БД нельзя.

Настраиваем REST API для проекта, подключаем представления

И последний штрих. Чтобы наконец увидеть в действии REST API, нужно подключить созданные ранее классы-представления: QueryModelViews, SearchEngineModelViews и PresetModelView. Для этого в файле Backend/urls.py подключим новый роутер:
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')),
)
Всё, готово. Давай посмотрим что получилось. Перейди по адресу localhost:8000/api/ , ты увидишь, что-то вроде этого:
Как видно из картинки мы успешно подключили и настроили наш роутер. Перейдём по любой из представленных ссылок, например localhost:8000/api/se/
Или например, давай посмотрим страницу пресетов, ради чего собственно это всё и затевалось. Итак, у меня есть две учётные записи dima и some и у каждой свои пресеты. Посмотрим как работает фильтрация по пользователю:
Пользователь some
Пользователь dima
Как видно, у первого пользователя только один пресет с id=3. А у второго 2 пресета с id=1 и 2.

Дополнительная настройка REST фреймворка

У тебя наверняка возник вопрос, что всё это красиво, но хочется просто получить JSON файл на стороне клиента и сделать то что должно. У меня так же зудело и я нашёл довольно элегантное решение для данной проблемы.
Смотри, в режиме разработки нам будут показываться предыдущие страницы на картинках, но если мы поменяем режим (т.е. сделаем debug=False в settings.py) мы будем получать вот такие вот ответы:
У меня в firefox стоит специальное расширение которое делает вот такую вот красоту. Расширение называется, если что, JSONView и доступно для Chrome тоже.
Чтобы добиться такого результата, достаточно добавить в конце файла settings.py следующие строчки:
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',
]
}
Мы просто меняем рендер по умолчанию и получаем то, что получаем ... JSON файл.

Работа с API со стороны клиента

Работать с API со стороны клиента, очень просто. Необходимо сделать соответствующий AJAX запрос и обработать ответ. Например, так я получу все доступные записи в SearchEngine модели.
axios.get("/api/se/", {
headers: {
"accept": "application/json",
'Content-Type': "application/json",
}
})
.then( resp => {
// Your code
})
.catch( error => {
// Your error code
}
Для примера, я хочу добавлять разные иконки статусов для каждого из поискового движка. Чтобы получить непосредственные данные из ответа, мы обратимся к специальному объекту data в ответе и после к необходимым нам полям. И в зависимости от статуса движка, мы либо добавим ему обработчик, либо нет.
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}`)
})
Да, я понимаю, этот блок кода гораздо сложнее, с вложенными друг в друга запросами. Но этот пример идеально подходит для демонстрации того, как можно получить данные из ответа.
А вот и ответы с сервера:
Для localhost:8000/api/se/
Для localhost:8000/api/seue/
И то, как это может выглядеть на сайте:

Заключение

Вот и закончили мы настройку API нашего сайта при помощи Django REST фреймворка. Конечно, по началу может показаться что всё это как-то запутанно и чересчур сложно, но поверь, когда твой проект станет чуть-чуть сложней, чем одностраничный лендинг, ты сразу вспомнишь про REST фреймворк и про то, что он не такой уж и запутанный ( •̀ ω •́ )y

Комментарии

(0)

captcha
Отправить
Сейчас тут пусто. Буть первым (o゚v゚)ノ

Другое

Использованные термины


Релевантные вопросы