DRF, часть 1 - Проектирование структуры API
Введение
Раз уж у меня не получается написать полноценную огромную статью про Django REST Framework, следует публиковать хотя бы небольшие заметки. Начать следует с прописных истин.
- 1. Дуб - дерево.
- 2. Олень - животное.
- 3. Смерь - неизбежна.
- 4.
api
- отдельное приложение в нашем проекте - 5. Следует придерживаться общепринятых правил именования частей API
- 6. API должен быть версионным
- 7. API должен быть простым
С первыми тремя пунктами всё ясно, поэтому перейду сразу к четвёртому и нижеследующим.
Структура каталогов и версионность
Сначала я пытался запихнуть API в разные части основного проекта, создавал в каталоге с приложениями файлы api.py
, serializers.py
и permissions.py
.
О том, насколько это плохая идея, я узнал почти сразу же, когда начал путаться с тем, что и где лежит. Стоило только переименовать один из каталогов, как сразу же всё начинало сыпаться. В итоге я пришёл к тому, что API - не просто отдельное приложение, содержащее только файлы __init__.py
и urls.py
(со ссылками на нужные файлы в приложениях Django), а полноценный модуль со множеством вложенных модулей. В общем, почувствуйте разницу:
/api/
__init__.py
urls.py
/news/
__init__.py
admin.py
api.py
models.py
serializers.py
tests.py
urls.py
views.py
/comments/
__init__.py
admin.py
api.py
models.py
serializers.py
tests.py
urls.py
views.py
/api/
/v1/
/news/
/comments/
__init__.pt
api.py
permissions.py
serializers.py
urls.py
__init__.py
api.py
permissions.py
serializers.py
urls.py
__init__.py
urls.py
/core/
/comment/
__init__.py
api.py
permissions.py
serializers.py
urls.py
/news/
/comments/
__init__.py
admin.py
models.py
tests.py
views.py
__init__.py
admin.py
models.py
tests.py
views.py
Надеюсь, структура понятна. Во-первых, всё, что связано с API, переехало в одноимённое приложение. Во-вторых, API теперь поддерживает версионность. Для этого нужно всего ничего - создать соответствующие каталоги и правильно описать файл urls.py
в самом начале. Я сделал так:
from django.conf.urls import include
from django.conf.urls import url
urlpatterns = [
url(r'', include('api.v2.urls')),
url(r'^v1/', include('api.v1.urls')),
url(r'^v2/', include('api.v2.urls')),
]
Естественно, где-то в главном файле urls.py
есть строка, в которой конфигурация URL для API присоединяется к общей конфигурации URL через всё тот же include()
.
Как видно из этого файла, если пользователь нашего API не указывает версию, он использует последнюю доступную. В то же время, при необходимости можно указать любую из имеющихся. В одной из статей я видел тезис, согласно которому компании, которые заботятся о своих клиентах и хотят сделать свой сервис успешным, крайне редко или вообще никогда не меняют API. Естественно, не меняеть его вообще никогда вряд ли получится, но можно сделать различия минимальными или предоставить возможность в течение длительного времени использовать старый API, т.к. разработчикам интересней писать что-то новое, а не переписывать старое только потому, что теперь какой-то метод в нашем API перестал работать.
Именование API
Сначала хочу сказать, как делать НЕ НАДО:
http://example.org/api/
Это что угодно, но не API. К сожалению, по ночам мне всё ещё снятся кошмары, в которых я вижу, как на одном из сайтов общение с сервисами сделано именно так.
GET /api/news/get/?id=1 - получить новость с id=1
GET /api/news/get/all/ - получить все новости
POST /api/news/add/ - добавить новость
POST /api/news/update/?id=1 - обновить новость с id=1
POST /api/news/delete/?id=1 - удалить новость с id=1
GET /api/news/?id=1 Получение записи с id=1
POST /api/news/?id=1?action=update Обновление записи с id=1
POST /api/news/?id=1?action=delete Удаление записи с id=1
Это API? Конечно же, нет! Суть REST-сервисов в том, что требуемое действие определяется HTTP-заголовком!
URL - /api/news/:id
GET /api/news/ - получить список всех новостей
POST /api/news/ - создать новость
GET /api/news/12/ - вернёт новость с id=12
PATCH /api/news/12/ - обновить новость с id=12
DELETE /api/news/12/ - удалить новость с id=12
Разница, как говорится, налицо.
В Django REST Framework обновление записи может быть произведено двумя способами - полностью или частично. В первом случае вызывается запрос с заголовком PUT
, во втором - PATCH
.
При полном обновлении сериализатор проверяет заполнение всех полей модели, у которых не указаны свойства null=True
. Не будем забывать, что для модели пользователя обязательными для заполнения являются поля "Пароль" и "Имя пользователя". Но мы же всего лишь хотели указать новую дату рождения! Почему DRF проверяет все поля? Потому что надо было вызывать метод PATCH
.
Теперь пора поговорить, как делать лучше (моё мнение по данному вопросу актуально только на момент написания статьи и в будущем может быть пересмотрено).
/api/articles/:id/ - статьи
/api/friends/:id/ - друзья
/api/news/:id/ - новости
/api/users/:id/ - пользователи
/api/videos/:id/ - видео
GET /api/acticles/:id/comments/ - получить комментарии к статье
GET /api/news/:id/comments/ - получить комментарии к новости
POST /api/articles/:id/comments/ - добавить комментарий к статье
POST /api/news/:id/comments/ - добавить комментарий к новости
DELETE /api/comments/articles/:id/ - удалить комментарий к статье
DELETE /api/comments/news/:id/ - удалить комментарий к новости
Сериализаторы и всё остальное.
Я считаю, что при написании новой версии API могут измениться поля, с которыми работают сериализаторы, поэтому для каждой версии их лучше создавать заново. Кроме того, сами методы для работы с данными могут стать другими. Отсюда следует простой вывод (до которого мне пришлось доходить своим умом пару месяцев):
Простота API - почти недостижимый идеал. Чтобы сделать его простым, придётся проделать очень сложную работу. Не удивляйтесь. Кнопка включения на корпусе компьютера - очень простой интерфейс, однако, по одному нажатию на неё выполняется огромное количество самых разнообразных и сложных действий, на глубокое понимание которых некоторые люди тратят большую часть своей жизни. Таким же должен быть и ваш API - всегда оставаться простым для внешнего наблюдателя, не смотря на то, какая бы сложная работа не происходила внутри системы.
Итоги
- Разработчики хотят работать с сущностями, а не с URL. Сделайте интуитивно понятными все URL вашего API.
- Разработчики ленивы и не хотят изучать 34 параметра, влияющие на результат вызова одного единственного метода (из 56, имеющихся в наличии). Старайтесь избегать реализации поведения API через параметры.
limit
,offset
,filter
иsorting
- необходимое зло, от них никуда не деться. - Действие, выполняемое методом API, должно зависеть от HTTP-заголовка.
GET
для чтения,POST
для создания,PUT/PATCH
для обновления иDELETE
для удаления записей. - Пишите документацию к вашему API. Даже самая лучшая структура URL для API неспособна описать различные тонкости и нюансы его работы.
Огромное спасибо за статью!
ОтветитьУдалитьМаксим, только что попал на ваш сайт (случайно). Жалею что не нашел раньше. Статьи очень интересные. Продолжайте в том же духе. Спасибо.
ОтветитьУдалитьСпасибо за статью!
ОтветитьУдалитьЯ вот только по структуре не понял: сериализаторы и пермишены есть и в /api/v1/news/comments/ и в /core/comment/
ОтветитьУдалитьЗачем?
Статья была написана давно, а я с тех пор частично пересмотрел свои взгляды. Теперь сериализаторы создаю прямо в приложении, т.е. serializers.py лежит рядом с models.py, самих сериализаторов стало больше, обычно 3 - для списка, для детального просмотра и для записи. Структура API сейчас вся строится на ViewSet'ах, но это тема отдельной большой статьи.
УдалитьЖдём новоую большую статью :-)
Удалить