Хабрахабр

[Перевод] Рассказ о том, как не надо проектировать API

Однажды я помогал товарищу, которому нужно было интегрировать с сайтом его клиента данные о свободном и занятом жилье из системы управления имуществом. К моей радости у этой системы было API. Но, к сожалению, устроено оно было из рук вон плохо.

image

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

Обзор ситуации

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

Проблема №1: формат тела запроса

Так как клиенту интересны только сведения о том, свободен ли, скажем, гостиничный номер, или занят, нас интересует лишь обращение к конечной точке API /getAvailabilities. И, хотя обращение к подобному API должно приводить к получению данных о доступности комнат, это обращение, на самом деле, выглядит как POST-запрос, так как автор API решил оснастить его возможностью принимать, в виде JSON-тела запроса, фильтры. Вот список возможных параметров запроса и примеры принимаемых ими значений:

{ "checkIn": "20151001", "lastNight": "20151002", "checkOut": "20151003", "roomId": "12345", "propId": "1234", "ownerId": "123", "numAdult": "2", "numChild": "0", "offerId": "1", "voucherCode": "", "referer": "", "agent": "", "ignoreAvail": false, "propIds": [ 1235, 1236 ], "roomIds": [ 12347, 12348, 12349 ]
}

Пройдёмся по этому JSON-объекту и поговорим о том, что здесь не так.

  1. Даты (checkIn, lastNight и checkOut) представлены в формате YYYYMMDD. Тут нет абсолютно никакой причины для того, чтобы не использовать стандартный формат ISO 8601 (YYYY-MM-DD) при преобразовании дат в строки, так как это — широко применяемый стандарт представления дат. Он знаком многим разработчикам, именно его ожидают получить на вход многие JSON-парсеры. Кроме того, возникает ощущение, что поле lastNight является избыточным, так как тут имеется поле checkOut, которое всегда представлено датой, на один день опережающей дату, заданную в lastNight. В связи с отмеченными выше недостатками предлагаю, при проектировании подобных API, стремиться к тому, чтобы всегда использовать стандартные способы представления дат и стараться не обременять пользователей API необходимостью работы с избыточными данными.
  2. Все поля-идентификаторы, а также поля numAdult и numChild, являются числовыми, но представлены в виде строк. В данном случае для представления их в виде строк нет никакой видимой причины.
  3. Здесь можно заметить следующие пары полей: roomId и roomIds, а так же propId и propIds. Наличие полей roomId и propId является избыточным, так как и то и другое можно использовать для передачи идентификаторов. Кроме того, тут можно заметить проблему с типами. Обратите внимание на то, что поле roomId является строковым, а в массиве roomIds нужно использовать числовые значения идентификаторов. Это может привести к путанице, к проблемами с парсингом, и, кроме того, говорит о том, что на сервере некоторые операции выполняются со строками, а некоторые с числами, несмотря на то, что эти строки и числа используются для представления одних и тех же данных.

Мне хотелось бы предложить разработчикам API стараться не усложнять жизнь тем, кто этими API будет пользоваться, допуская при проектировании API ошибки, подобные вышеописанным. А именно, стоит стремиться к стандартному форматированию данных, к тому, чтобы они не были бы избыточными, к тому, чтобы для представления однородных сущностей не использовались бы разные типы данных. И не стоит всё, без разбора, представлять в виде строк.

Проблема №2: формат тела ответа

Как уже было сказано, нам интересна лишь конечная точка API /getAvailabilities. Давайте посмотрим на то, как выглядит ответ этой конечной точки, и поговорим о том, какие недочёты допущены при его формировании. Помните о том, что нас, при обращении к API, интересует список идентификаторов объектов, свободных в заданный период времени и способных вместить заданное количество людей. Ниже приведён пример тела запроса к API и пример того, что оно выдаёт в ответ на этот запрос.

Вот запрос:

{ "checkIn": "20190501", "checkOut": "20190503", "ownerId": "25748", "numAdult": "2", "numChild": "0"
}

Вот ответ:

, "13219": { "roomId": "13219", "propId": "5729", "roomsavail": "0" }, "14900": { "roomId": "14900", "propId": "6779", "roomsavail": 1 }, "checkIn": "20190501", "lastNight": "20190502", "checkOut": "20190503", "ownerId": 25748, "numAdult": 2
}

Поговорим о проблемах ответа.

  1. В теле ответа свойства ownerId и numAdult внезапно стали числами. А в запросе нужно было указывать их в виде строк.
  2. Список объектов недвижимости представлен в виде свойств объекта, ключами которых являются идентификаторы комнат (roomId). Логично было бы ожидать того, что подобные данные выводились бы в виде массива. Для нас это означает, что для того чтобы получить список доступных комнат, нужно перебрать весь объект, проверяя при этом наличие у вложенных в него объектов определённых свойств, вроде roomsavail, и не обращая внимания на что-то вроде checkIn и lastNight. Затем нужно было бы проверить значение свойства roomsavail, и, если оно больше 0, можно было бы сделать вывод о том, что соответствующий объект доступен для бронирования. А теперь давайте присмотримся к свойству roomsavail. Вот какие варианты его представления встречаются в теле ответа: "roomsavail": "0" и "roomsavail": 1. Видите закономерность? Если комнаты заняты — значение свойства представлено строкой. Если свободны — оно превращается в число. Это способно привести к множеству проблем в языках, строго относящихся к типам данных, так как в них одно и то же свойство не должно принимать значения разных типов. В связи с вышесказанным мне хотелось бы предложить разработчикам использовать массивы JSON-объектов для представления неких наборов данных, а не применять для этой цели неудобные конструкции в виде пар ключ-значение, подобные той, что мы тут рассматриваем. Кроме того, нужно следить за тем, чтобы поля однородных объектов не содержали бы данные разных типов. Правильно отформатированный ответ сервера мог бы выглядеть так, как показано ниже. Обратите внимание и на то, что при представлении данных в таком виде сведения о комнатах не содержат дублирующихся данных.

{ "properties": [ { "id": 4478, "rooms": [ { "id": 12328, "available": false } ] }, { "id": 5729, "rooms": [ { "id": 13219, "available": false } ] }, { "id": 6779, "rooms": [ { "id": 14900, "available": true } ] } ], "checkIn": "2019-05-01", "lastNight": "2019-05-02", "checkOut": "2019-05-03", "ownerId": 25748, "numAdult": 2
}

Проблема №3: обработка ошибок

Вот как организована обработка ошибок в рассматриваемом здесь API: на все запросы система отправляет ответы с кодом 200 — даже в том случае, если произошла ошибка. Это означает, что единственный способ отличить нормальный ответ от ответа с сообщением об ошибке заключается в разборе тела ответа и в проверке наличия в нём полей error или errorCode. В API предусмотрены лишь следующие 6 кодов ошибок.

Коды ошибок API Beds24

Пойти на такой шаг можно лишь в том случае, если это предусмотрено фреймворком, на базе которого вы разрабатываете API. Предлагаю всем, кто это читает, постараться не возвращать ответ с кодом 200 (успешная обработка запроса) в том случае, если при обработке запроса что-то пошло не так. Возврат адекватных кодов ответов позволяет клиентам API заранее знать о том, нужно ли им парсить тело ответа или нет, и о том, как именно это делать (то есть — разбирать ли обычный ответ сервера или объект ошибки).

В нашем случае улучшить API в этом направлении можно двумя способами: можно либо предусмотреть особый HTTP-код в диапазоне 400-499 для каждой из 6 возможных ошибок (лучше всего поступить именно так), либо возвращать, при возникновении ошибки, код 500, что позволит клиенту, по меньшей мере, знать перед разбором тела ответа о том, что оно содержит сведения об ошибке.

Проблема №4: «инструкции»

Ниже приведены «инструкции» по использованию API из документации проекта:

Пожалуйста изучите следующие инструкции при использовании API.

  1. Обращения к API следует проектировать так, чтобы в ходе их выполнения приходилось бы отправлять и принимать минимальный объём данных.
  2. Обращения к API выполняются по одному за раз. Необходимо дождаться выполнения очередного обращения к API прежде чем выполнять следующее обращение.
  3. Если нужно выполнить несколько обращений к API, между ними следует предусмотреть наличие паузы длительностью несколько секунд.
  4. Вызовы API нужно выполнять не слишком часто, поддерживая уровень обращений на минимальном уровне, необходимом для решения задач клиента.
  5. Чрезмерное использование API в пределах 5-минутного периода приведёт к блокировке вашей учётной записи без дополнительных уведомлений.
  6. Мы оставляем за собой право блокировать доступ к системе клиентам, которые, по нашему мнению, чрезмерно используют API. Делается это по нашему усмотрению и без дополнительных уведомлений.

В то время как пункты 1 и 4 выглядят вполне обоснованными, с другими пунктами этой инструкции я согласиться не могу. Рассмотрим их.

  1. Пункт №2. Если вы разрабатываете REST API, то предполагается, что это будет API, не зависящее от состояния. Независимость обращений к API от предыдущих обращений к нему — это одна из причин того, что технология REST нашла широкое применение в облачных приложениях. Если некий модуль системы не поддерживает состояние, его, в случае ошибки, можно легко развернуть повторно. Системы, основанные на подобных модулях, легко масштабируются при изменении нагрузки на них. При проектировании RESTful API стоит следить за тем, чтобы это было API, не зависящее от состояния, и чтобы тем, кто его использует, не приходилось бы беспокоиться о чём-то вроде выполнения только одного запроса за раз.
  2. Пункт №3. Этот пункт выглядит довольно странно и неоднозначно. Я не могу понять причину, по которой был написан этот пункт инструкции, но у меня возникает ощущение, что он говорит нам о том, что в процессе обработки запроса система выполняет некие действия, и, если её при этом «отвлечь» ещё одним запросом, отправленным не вовремя, это может нарушить её работу. Кроме того, то, что автор руководства говорит о «нескольких секундах», не позволяет узнать точной длительности паузы, которую нужно выдержать между последовательными запросами.
  3. Пункты №5 и №6. Тут говорится о «чрезмерном использовании API», но никаких критериев «чрезмерного использования» не приводится. Может, это 10 запросов в секунду? А может — 1? Кроме того, некоторые веб-проекты могут иметь огромные объёмы трафика. Если без каких-то адекватных причин и без уведомлений закрывать им доступ к нужным им API, их администраторы, наверняка, от использования таких API откажутся. Если вам доведётся писать подобные инструкции — используйте в них чёткие формулировки и ставьте себя на место пользователей, которым придётся работать с вашей системой, руководствуясь вашими инструкциями.

Проблема №5: документация

Вот как выглядит документация к API.

Документация к API Beds24

Она выглядела бы гораздо лучше, если бы её хорошо отформатировали. Единственная проблема этой документации — её внешний вид. На мой взгляд, он выглядит гораздо лучше вышеприведённого. Специально для того, чтобы показать возможный внешний вид подобной документации, я, воспользовавшись Dillinger, и потратив на это меньше двух минут, сделал следующий её вариант.

Улучшенный вариант документации

Если речь идёт о простых документах, похожих на вышеописанный, то для их оформления вполне достаточно чего-то вроде обычного markdown-файла. Для создания подобных материалов рекомендуется пользоваться специальными инструментами. Если документация устроена сложнее, то для её оформления лучше всего воспользоваться инструментами наподобие Swagger или Apiary.

Кстати, если сами хотите взглянуть на документацию к API Beds24 — загляните сюда.

Проблема №6: безопасность

В документации ко всем конечным точкам API сказано следующее:

Это делается в меню SETTINGS → ACCOUNT → ACCOUNT ACCESS. Для использования этих функций должен быть разрешён доступ к API.

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

Ключ доступа к API можно установить, воспользовавшись меню SETTINGS → ACCOUNT → ACCOUNT ACCESS. Большинство JSON-методов требуют ключ API для доступа к учётной записи.

Длина ключа должна быть в пределах от 16 до 64 символов. В дополнение к непонятному разъяснению вопросов аутентификации оказывается, что ключ для доступа к API пользователь должен создавать самостоятельно (делается это, кстати, путём ручного заполнения соответствующего поля, какие-то средства для автоматического создания ключей не предусмотрены). В подобной ситуации возможны и проблемы, связанные с содержимым ключей, так как в поле для ключа можно ввести всё что угодно. Если позволить пользователям самостоятельно создавать ключи для доступа к API, это может привести к появлению весьма небезопасных ключей, которые можно легко подобрать. При проектировании API не позволяйте пользователям создавать ключи для доступа к API самостоятельно. В худшем случае это может привести к атаке на сервис методом SQL-инъекции или к чему-то подобному. Пользователь не должен иметь возможности изменить содержимое такого ключа, но, при необходимости, он должен иметь возможность сгенерировать новый ключ, признав старый недействительным. Вместо этого генерируйте для них ключи автоматически.

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

Пример аутентификации в API Beds24

После этого он извлекает ключ, выполняет аутентификацию, а затем уже решает — что ему делать с запросом — выполнять его или нет. Если токен аутентификации передаётся в теле запроса — это означает, что серверу, прежде чем он доберётся до ключа, нужно будет разобрать тело запроса. А вот если аутентифицировать запрос не получилось, то ценное процессорное время будет потрачено на разбор тела запроса впустую. Если аутентификация удалась — то сервер не будет подвержен дополнительной нагрузке, так как в таком случае тело запроса всё равно пришлось бы разбирать. При таком подходе серверу понадобится разбирать тело запроса лишь в том случае, если аутентификация окажется успешной. Лучше будет отправлять токен аутентификации в заголовке запроса, используя что-то наподобие схемы аутентификации Bearer. Ещё одной причиной, по которой для аутентификации рекомендуется использовать стандартную схему наподобие Bearer, является тот факт, что с подобными схемами знакомо большинство разработчиков.

Проблема №7: производительность

Эта проблема в моём списке последняя, но это не умаляет её важности. Дело в том, что на выполнение запроса к рассматриваемому API уходит немного больше секунды. В современных приложениях такие задержки могут оказаться недопустимыми. Собственно говоря, тут можно посоветовать всем, кто занимается разработкой API, не забывать о производительности.

Итоги

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

Следите за тем, чтобы документация к API полно описывала бы их возможности, чтобы она была бы понятной и хорошо оформленной. Поэтому я хотел бы попросить всех, кто проектирует API, думать о том, как с ним будут работать пользователи их сервисов. Кроме того, не забывайте о безопасности и о правильной обработке ошибок. Контролируйте именование сущностей, обращайте внимание на то, чтобы данные, которые выдаёт или принимает ваше API, были бы чётко структурированными, чтобы с ними было бы легко и удобно работать. Если при проектировании API принять во внимание всё то, о чём мы говорили, тогда для работы с ним не понадобится писать нечто вроде тех странных «инструкций», которые мы обсуждали выше.

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

Уважаемые читатели! Встречались ли вам некачественно спроектированные API?

Показать больше

Похожие публикации

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»