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'ах, но это тема отдельной большой статьи.
УдалитьЖдём новоую большую статью :-)
Удалить