Проектирование 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 не сможет учесть все необходимые варианты и требования.