Делюсь опытом в описанных технологиях. Блог в первую очередь выполняет роль памяток для меня самого.

DRF, часть 1 - Проектирование структуры API

6 комментариев

Введение

Автор не является экспертом с мировым именем в данном вопросе и всего лишь публикует здесь свои мысли, которые кажутся ему правильными на момент написания. Для более полной информации о работе с Django REST Framework обращайтесь к официальной документации.

Раз уж у меня не получается написать полноценную огромную статью про Django REST Framework, следует публиковать хотя бы небольшие заметки. Начать следует с прописных истин.

  1. 1. Дуб - дерево.
  2. 2. Олень - животное.
  3. 3. Смерь - неизбежна.
  4. 4. api - отдельное приложение в нашем проекте
  5. 5. Следует придерживаться общепринятых правил именования частей API
  6. 6. API должен быть версионным
  7. 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 в самом начале. Я сделал так:

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

Сначала хочу сказать, как делать НЕ НАДО:

Вешать всё на один URI и в зависимости от содержимого полученных пакетов выполнять то или иное действие либо возвращать нужный набор данных.
Пример:
http://example.org/api/

Это что угодно, но не API. К сожалению, по ночам мне всё ещё снятся кошмары, в которых я вижу, как на одном из сайтов общение с сервисами сделано именно так.

Использовать глаголы в частях URL или для выполнения определённых действий.
Пример
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

Разница, как говорится, налицо.

Теперь пора поговорить, как делать лучше (моё мнение по данному вопросу актуально только на момент написания статьи и в будущем может быть пересмотрено).

Названия сущностей во множественном числе. Каждой сущности - отдельный URL.
Пример

/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 не изменятся.

Простота API - почти недостижимый идеал. Чтобы сделать его простым, придётся проделать очень сложную работу. Не удивляйтесь. Кнопка включения на корпусе компьютера - очень простой интерфейс, однако, по одному нажатию на неё выполняется огромное количество самых разнообразных и сложных действий, на глубокое понимание которых некоторые люди тратят большую часть своей жизни. Таким же должен быть и ваш API - всегда оставаться простым для внешнего наблюдателя, не смотря на то, какая бы сложная работа не происходила внутри системы.

Итоги

  • Разработчики хотят работать с сущностями, а не с URL. Сделайте интуитивно понятными все URL вашего API.
  • Разработчики ленивы и не хотят изучать 34 параметра, влияющие на результат вызова одного единственного метода (из 56, имеющихся в наличии). Старайтесь избегать реализации поведения API через параметры. limit, offset, filter и sorting - необходимое зло, от них никуда не деться.
  • Действие, выполняемое методом API, должно зависеть от HTTP-заголовка. GET для чтения, POST для создания, PUT/PATCH для обновления и DELETE для удаления записей.
  • Пишите документацию к вашему API. Даже самая лучшая структура URL для API неспособна описать различные тонкости и нюансы его работы.

6 комментариев :

  1. Огромное спасибо за статью!

    ОтветитьУдалить
  2. Максим, только что попал на ваш сайт (случайно). Жалею что не нашел раньше. Статьи очень интересные. Продолжайте в том же духе. Спасибо.

    ОтветитьУдалить
  3. Я вот только по структуре не понял: сериализаторы и пермишены есть и в /api/v1/news/comments/ и в /core/comment/
    Зачем?

    ОтветитьУдалить
    Ответы
    1. Статья была написана давно, а я с тех пор частично пересмотрел свои взгляды. Теперь сериализаторы создаю прямо в приложении, т.е. serializers.py лежит рядом с models.py, самих сериализаторов стало больше, обычно 3 - для списка, для детального просмотра и для записи. Структура API сейчас вся строится на ViewSet'ах, но это тема отдельной большой статьи.

      Удалить
    2. Ждём новоую большую статью :-)

      Удалить