REST API: Проектирование

Проектирование API — важная часть разработки и хорошая инвестиция в успешный результат. Соблюдение принципов проектирования поможет сделать API современным и удобным в работе. Начнём с принципов консистентности и расширяемости.

Консистентность

Консистентность — это согласованность данных друг с другом, их целостность и внутренняя непротиворечивость. Например, данные о каком-то объекте, полученные с одного эндпоинта, не должны отличаться от данных о том же объекте, но полученных с другого эндпойнта.

Другой случай соблюдения принципа консистентности: одинаковые типы данных должны быть описаны одинаково, где бы они ни использовались.В проекте Yatube есть модель Post. Немного упростим эту модель и рассмотрим работу API на её примере.

# Тестовая модель
class Post(models.Model):
    text = models.TextField()
    group = models.ForeignKey(Group, on_delete=models.CASCADE, blank=True, null=True)
    pub_date = models.DateTimeField('Дата публикации', auto_now_add=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE) 

В этой модели есть поле DateTimeField, данные из него будут возвращаться в ответах. Но JSON позволяет передавать дату в любом виде, и разработчик сам решает, какой формат даты выбрать.

«Пусть это будет unix-timestamp», — решил разработчик:

# GET запрос поста с id=10
GET /api/v1/posts/10/

# Ответ API
[[10, "Это текст из моего поста.", 1618567801, 1]] 

Когда формат даты выбран — в дело вступает принцип консистентности, который предполагает, что и все другие запросы должны возвращать дату именно в этом формате.

Пример нарушения консистентности — в следующем листинге: при запросе комментария дата возвращается строкой.

# GET запрос первого комментария к посту с id=10
GET /api/v1/posts/10/comments/1/

# Ответ API c датой в формате строки - консистентность нарушена
[[1, "Это мой первый комментарий.", "2020-03-23T18:02:33.123543Z", 10]] 

Это непорядок, надо переделать.Если же разработчик не забыл о консистентности — ответ API будет таким:

# GET запрос первого комментария к посту с id=10
GET /api/v1/posts/10/comments/1

# Ответ API c датой в формате unix-timestamp
[[1, "Это мой первый комментарий.", 1618565516, 10]] 

Вот теперь разработчик молодец.

Согласованность

Понятие консистентности включает в себя и идею согласованности: добавление в API новой функциональности не должно сломать API.

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

# GET запрос поста с id=10
GET /api/v1/posts/10/

# Ответ API
[[10, "Это текст из моего поста.", 1618567801, 1]] 

Клиенты успешно принимают ответ и ожидают, что элемент списка с индексом 0 — это id объекта, а под индексом 2 хранится время в формате unix-timestamp.

Но в какой-то момент потребовалось расширить ответ и дополнительно возвращать в ответе id группы, в которой был опубликован пост. Разработчик изменяет ответ, добавив в него новый элемент:

# Новый ответ API
[[10, "Это текст из моего поста.", 7, 1618567801, 1]] 

На первый взгляд ничего страшного не произошло, но теперь в элементе с индексом 2 хранится не время, а id группы; в результате все приложения, которые подключены к API, перестанут работать.

Можно предположить, что именно в этот момент в службу поддержки начнут писать и звонить недовольные клиенты: «Ваш API сломался!». А он не сломался — он просто начал работать по-другому. Но клиентам от этого не легче.

Нужно искать выход из сложившейся ситуации. Можно создать новую версию API и применить изменения в ней, оставив существующую версию в прежнем виде. Такая практика встречается повсеместно (а иногда это вообще единственный доступный вариант).

Однако будет лучше спроектировать API так, чтобы можно было расширять его, не создавая новые версии и не лишая клиентов возможности получать данные в привычном формате.

Расширяемость

При проектировании API именно разработчик определяет, какие данные и в каком формате будут возвращаться в ответ на тот или иной запрос.

Сейчас API возвращает данные в виде упорядоченного списка: [[1, "Это мой первый комментарий.", 1618565516, 10]]. Так иногда делают для повышения производительности, но в нашей ситуации этот вариант оказался непрактичен: он усложняет расширяемость. Здесь лучше отдавать данные в более распространённом для JSON формате, аналогичном словарю, со структурой {"ключ": "значение"}.

При такой структуре добавление новых элементов не повлечёт за собой проблем с парсингом данных. Клиенты будут получать необходимую информацию из ответа по прежним ключам, а новые ключи могут использовать, а могут игнорировать. И ничего не сломается.

# Ответ API в формате "ключ": "значение"
[{
    "id": 10,
    "text": "Это текст из моего поста.",
    "pub_date": 1618567801,
    "group_id": 7,
    "author_id": 1
}] 

Уже неплохо. Но лучше бы предусмотреть и следующий уровень расширяемости: что, если потребуется добавить к информации о посте какие-то данные, которых нет в модели Post?

Например, при запросе GET /api/v1/posts/10 возвращаемую информацию о посте с id=10 можно расширить дополнительными данными: это может быть, допустим, набор ссылок на посты, похожие на запрошенный.

Этот случай можно и нужно учесть на этапе проектирования. Лучше сразу предусмотреть возможность добавления в ответ дополнительных (но не являющихся частью модели) данных. Для этого нужно немного изменить структуру ответа.

В нашем примере для добавления списка ссылок к ответу нужно:

  • выделить возвращаемую информацию о посте в JSON-объект post;
  • добавить в возвращаемый JSON объект links, в котором будут перечислены нужные ссылки.

Такой подход позволит добавлять в ответ и другие объекты без опасения сломать парсеры у клиентов.

Итоговый вариант может быть примерно таким:

// Ответ API в формате "ключ": "значение" с ссылками на похожие посты
[{
    "post": {
        "id": 10,
        "text": "Это текст из моего поста.",
        "pub_date": 1618567801,
        "group_id": 7,
        "author_id": 1
    },
    "links": {
        "link1": "/posts/12",
        "link2": "/posts/23",
            ...
    }
}] 

Такая архитектура даст возможность в будущем включать в ответ любую дополнительную информацию, пусть даже сейчас неизвестно, что это будут за данные и откуда они возьмутся.

Вот это и называется расширяемостью. Тщательное проектирование API даст возможность расширять ответы без опасности поломать что-то уже существующее.

Самая лучшая структура JSON для API

В природе не существует «самой лучшей», эталонной структуры ответа API. Нельзя корректно ответить на вопрос «что лучше — велосипед или чайник?», всё зависит от ситуации; точно так же и для каждого проекта существует своя лучшая архитектура. И одна из задач разработчика — найти и реализовать её.

Для одного проекта лучшей структурой может быть тот плоский список, с которого мы начинали: ответ получается лаконичен, это экономит ресурсы и трафик; для другого проекта даже структура нашего финального JSON не сможет учесть все необходимые варианты и требования.





Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: