Запрос к API проходит такой путь:
- urls.py — здесь настраивается маршрутизация API, описываются
эндпоинты
для ресурсов. - serializers.py — здесь данные преобразуются из JSON в объекты Python и обратно;
- views.py — здесь идёт обработка параметров запроса, получение или запись данных, создание ответа.
Для обработки запросов к API применяются view-функции или view-классы. Функции, как правило, позволяют лучше понять, что происходит в коде; вот с них и начнём.
View-функции API
Чтобы создать view-функцию для API, нужно обеспечить её работу со специальными объектами Request и Response, которые, по сути, являются модификациями объектов HttpRequest и HttpResponse, используемых в Django.В DRF у объекта request появляется атрибут data, он чем-то похож на уже знакомый вам request.POST. Отличия заключаются в том, что request.
POST содержит только данные из формы и работает только с методом POST. А request.data содержит словарь со всеми данными из запроса и работает с методами POST, PUT и PATCH.
View-функция API должна возвращать объект Response, которому передаются сериализованные данные. В нашем случае «сериализованные данные» — это JSON: ведь мы работаем с архитектурой REST.
Декоратор @api_view
Для «перенастройки» view-функции на работу с API в Django REST framework есть стандартный декоратор @api_view.Среди возможностей, которые он предоставляет, выделим следующие:
- @api_view определяет разрешённые методы запросов;
- @api_view позволяет оперировать модифицированными объектами Request и Response.
Рассмотрим простую view-функцию hello()
в Django: при GET-запросе она вернёт ответ в текстовом формате: «Это был GET-запрос!», а при POST-запросе — вернёт строку 'Получены данные: <словарь>'
со словарём request.POST
.
from django.http import HttpResponse
def hello(request):
if request.method == 'POST':
return HttpResponse(f'Получены данные: {request.POST}')
elif request.method == 'GET':
return HttpResponse('Это был GET-запрос!')
Чтобы превратить hello()
во view-функцию для API, нужно декорировать её и немного изменить ответы. В параметрах декоратора @api_view указываются типы запросов, которые разрешены для декорируемой view-функции: @api_view([<разрешённые HTTP-методы>])
.
from rest_framework.decorators import api_view # Импортировали декоратор
from rest_framework.response import Response
@api_view(['GET', 'POST']) # Добавили декоратор и указали разрешённые методы
def hello(request):
if request.method == 'POST':
# Изменили тип возвращаемого объекта,
#теперь это Response и вложенный JSON
return Response({'message': 'Получены данные', 'data': request.data})
# Изменили ответ
return Response({'message': 'Это был GET-запрос!'})
В декораторе @api_view
указаны два разрешённых метода — GET и POST. В ответ на запросы других типов view-функция вернёт ответ “Error 405 Method Not Allowed” — это предусмотрено в декораторе @api_view.
- Данные из POST-запроса передаются в декорированную view-функцию
hello()
в словаре request.data. В ответ на POST-запрос view-функцияhello()
вернёт JSON с данными, которые были получены в запросе. - В ответ на GET-запрос view-функция вернёт заранее заготовленную фразу:
# Ответ на GET-запрос
{
"message": "Это был GET-запрос!"
}
Для наглядности в ответ добавлены ключи 'message'
и 'data'
, имена ключей в этом примере ни на что не влияют.Готово: простенькая view-функция API готова обрабатывать запросы!
View-функция API: валидация и сериализация отдельного объекта и списка объектов
View-функция hello()
достаточно проста — она просто возвращает полученные данные, не обрабатывая и даже не проверяя их: что передали — на том и спасибо.
В реальном мире view-функция должна быть сложнее; на примере cat_list()
из Kittygram разберёмся, как работает этот механизм.В модели Cat хранится имя котика name
, его цвет color
и год рождения birth_year
.
Начнём с создания новой записи в БД: передадим в POST-запросе новый объект на эндпоинт cats/
. При получении запроса данные из него сохраняются в словарь request.data
:
# request.data
{
"name": "Стёпа",
"color": "белый",
"bitrh_year": 1971
}
Прежде чем сохранить эти данные в базе, нужно их валидировать: удостовериться, что они соответствуют ожиданиям модели Cat. Данные из запроса передаются сериализатору, связанному с этой моделью: он провалидирует их и, если валидация пройдёт успешно, десериализует.Сериализатор уже готов: это CatSerializer.
# kittygram/cats/serializers.py
from rest_framework import serializers
from .models import Cat
class CatSerializer(serializers.ModelSerializer):
class Meta:
model = Cat
# В прошлом уроке fields = '__all__' изменили:
fields = ('name', 'color', 'birth_year')
При создании экземпляра сериализатора в конструктор класса передаём словарь request.data
, в котором содержатся данные из запроса. Именно их нужно валидировать и сериализовать.Данные передаются в сериализатор через именованный параметр data
.
# View-функция
@api_view(['GET', 'POST'])
def cat_list(request):
if request.method == 'POST': # Сейчас нас интересует только POST-запрос.
serializer = CatSerializer(data=request.data)
# В ответ на GET-запрос сейчас вернётся ошибка 500 Internal Server Error
Валидация входящих данных
Для валидации полученных данных надо вызвать в сериализаторе метод is_valid()
. В зависимости от результатов валидации можно среагировать на запрос по-разному:
- если валидация прошла успешно — сохраним запись в БД при помощи метода
save()
и вернём созданный объект; - если валидация не пройдена — вернём объект
serializer.errors
, в нём будет сохранён словарь с возникшими при валидации ошибками.
@api_view(['GET', 'POST'])
def cat_list(request):
if request.method == 'POST':
serializer = CatSerializer(data=request.data)
if serializer.is_valid():
# Если полученные данные валидны — выполняем работу
# Например, сохраняем данные в базу через save() (как при работе с формами)
serializer.save()
# Возвращаем JSON с полученными данными и статус-код 200
return Response(serializer.data, status=status.HTTP_200_OK)
# Если данные не прошли валидацию — возвращаем сообщение об ошибке:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Обработка POST-запроса со списком объектов
Следующая задача: научить view-функцию API принимать и обрабатывать список объектов. Такой подход будет востребован: если владелец котофермы захочет добавить в базу всех своих котов, ему не придётся отправлять сотню отдельных запросов.
Для этого к API нужно отправить POST-запрос с несколькими объектами Cat. В этом случае в словарь request.data
будет передан не словарь из полей name
,color
и birth_year
, а список объектов, у каждого из которых будут собственные поля с такими названиями.
# request.data со списком объектов
[
{
"name": "Стёпа",
"color": "белый",
"birth_year": 1971
},
{
"name": "Мурка",
"color": "рыжий",
"birth_year": 2010
},
{
"name": "Пушок",
"color": "чёрный",
"birth_year": 2018
}
]
Чтобы сериализатор был готов принять список объектов, в конструктор сериализатора нужно дополнительно передать именованный параметр many=True
.
serializer = CatSerializer(data=request.data, many=True)
В результате обработка запроса пройдёт по тому же принципу, что и с одним объектом.
Если этот параметр не указан, сериализатор не станет обрабатывать список объектов и вернёт ошибку: "Invalid data. Expected a dictionary, but got list."
Обработка GET-запроса на получение списка объектов
Вернуть список объектов в ответ на GET-запрос тоже не проблема. При подготовке ответа view-функция должна получить из базы данных queryset, в котором будут храниться запрошенные объекты модели (например, все объекты модели Cat). Для сериализации ответа этот queryset передаётся в конструктор сериализатора первым параметром, а вторым указывается many=True
:
# Получаем все объекты модели
cats = Cat.objects.all()
# Передаём queryset в конструктор сериализатора
serializer = CatSerializer(cats, many=True)
В результате view-функция API cat_list()
примет такой вид:
@api_view(['GET', 'POST']) # Разрешены только POST- и GET-запросы
def cat_list(request):
# В случае POST-запроса добавим одну запись или список записей в БД
if request.method == 'POST':
serializer = CatSerializer(data=request.data, many=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# В случае GET-запроса возвращаем список всех котиков
cats = Cat.objects.all()
serializer = CatSerializer(cats, many=True)
return Response(serializer.data)
Если в сериализаторе не указан параметр many=True
, при обработке GET-запроса вернётся ошибка AttributeError
.
Создать vs обновить
Результат вызова метода save()
может быть разным: может быть создана новая запись в БД, а может быть обновлена существующая запись.
Для создания новой записи в сериализатор передаются только данные из запроса; объект будет создан при вызове метода save()
.
serializer = CatSerializer(data=request.data)
# Если вызвать serializer.save(), будет создан новый экземпляр
Для обновления существующей записи первым параметром в сериализатор передаётся тот объект модели, который нужно обновить. В этом случае вызов save() не приведёт к созданию нового объекта.
cat = cat.objects.get(id=id)
serializer = CatSerializer(cat, data=request.data)
# Если вызвать serializer.save(), будет обновлён существующий экземпляр cat
Сериализатор и метод PATCH
По умолчанию сериализатору должны быть переданы значения всех полей, перечисленных в его параметре fields
. Однако в PATCH-запросе могут быть переданы не все поля, а лишь те, которые нужно изменить. По умолчанию в такой ситуации сериализатор вызовет ошибку 400 Bad Request "This field is required."
: в запросе не передано обязательное поле!
Решить эту проблему поможет аргумент partial=True
: если при создании сериализатора указать этот аргумент — отсутствие в запросе обязательных полей не вызовет ошибку.
serializer = CatSerializer(cat, data=request.data, partial=True)
Разделение обязанностей между view-функциями API
Для API программой-минимум должна быть реализация шести операций для нужной модели:
- создание нового объекта;
- получение информации об объекте;
- удаление объекта;
- замещение объекта (целиком);
- изменение одного или нескольких полей объекта;
- получение списка объектов.
Хорошей практикой считается сгруппировать эти операции в две view-функции.
В одной view-функции реализуется создание нового объекта и запрос всех объектов коллекции, например:
- POST-запрос на адрес
cats/
создаст новую запись о котике; - GET-запрос на тот же адрес вернёт список всех котиков.
Вторая view-функция обрабатывает запросы для получения, изменения и удаления одиночного объекта:
- GET-запрос к адресу
cats/<pk>/
вернёт один объект по его id; - запросы PUT, PATCH или DELETE к тому же адресу перезапишут, изменят или удалят существующую запись о котике.
В результате в urls.py потребуется описать лишь два эндпоинта:
cats/
,cats/<int:pk>/
.
А ссылаться эти эндпоинты будут на две view-функции во views.py, например:cat_list()
,cat_detail()
.
Время кодить
Пришло время самостоятельно реализовать CRUD на Django REST framework с помощью view-функций API.
Прежде чем вы отправитесь в бой, повторим несколько важных моментов:
- View-функция обрабатывает только те запросы, которые указаны в декораторе
@api_view([<разрешённые HTTP-методы>])
. - Данные из POST-запроса сохраняются в словаре
request.data
и передаются в конструктор сериализатора через именованный параметрdata
:data=request.data
. - Вместе с ответом необходимо вернуть клиенту правильный статус-код. Список кодов можно уточнить здесь.