12 неочевидных правил проектирования REST API

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Раскрываем 12 кейсов проектирования спецификации REST API из практики red_mad_robot, которые помогут сэкономить время для разработки. А также объясняем, почему стоит следовать подходу contract first — писать спецификацию прежде кода.

Статей, объясняющих основы архитектурного стиля REST, больше, чем символов в этом тексте. Самих принципов всего шесть, но они дают большой простор для реализации задач разными способами. В этой статье ведущий backend-разработчик red_mad_robot Серёжа Ретивых делится дополнительными правилами, которые мы выработали и применяем у себя в red_mad_robot на практике. Начнём с общих терминов, а если вам всё это знакомо, перепрыгивайте сразу в блок «Рекомендации проектирования для REST API».


Что такое REST API и зачем он нужен

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

Сервер предоставляет клиенту API (англ. Application programming interface) — это способ взаимодействия с собой, а для его описания служат контракты. Они однозначно интерпретируют передаваемые данные, а также дают возможность понять, что сервер вообще умеет делать.

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

Сегодня самый популярный в веб-разработке подход — REST. REST API — не строгий стандарт, а набор общих рекомендаций, которым можно следовать. Поэтому он оставляет большое поле для вариантов в деталях. Про эти детали и пойдет речь в основной части статьи.

Если ваш сервис реализован в соответствии с этим подходом, его можно называть RESTful, и он будет предоставлять REST API. Но также REST API можно назвать документ спецификации, в котором описан такой контракт.

Спецификации тоже можно писать и форматировать по-разному. Поэтому по мере развития выработался стандарт OpenAPI — самый распространённый и понятный формат. Мы в red_mad_robot используем его.

Почему стоит писать спецификацию прежде кода

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

Мы в red_mad_robot стараемся писать спецификацию прежде кода, и вот почему.

Скорость разработки

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

Моковый сервер

Из спецификации можно автоматически генерировать актуальный моковый сервер и делать жизнь потребителей API ещё лучше.

Моковый сервер (англ. mock — заглушка) — временный прототип будущего сервера, который точно реализует контракт или его часть. Он не содержит никакой логики внутри, а каждый раз отвечает одними и теми же заранее подготовленными данными. В нашем случае эти подготовленные данные и есть данные примеров в спецификации.

Кастомизация API

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

Гибкость процесса

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

Дополнительный контроль схемы данных

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

Представим, что есть книга с идентификатором и автором. Если автор — просто строка, вложенных данных нет. А если автор, в свою очередь, имеет идентификатор, ФИО и возраст, и мы их также передаем внутри книги, то автор — вложенные данные.

Пример первый. Есть простой GET-запрос профиля пользователя, оценённый как мэппинг данных из одной таблицы базы данных в JSON-объект. Мы публикуем спецификацию, Frontend приходит с запросом — в дизайне ещё есть поле «Бонусы». И вот нам уже нужно делать интеграцию или асинхронно подготавливать данные.

Пример второй. Разработчики не любят писать лишний код. Если ранее был реализован сериализатор для модели А с вложенным объектом В, то дальше он везде и будет использоваться. Необходимости в В для конкретного эндпоинта может и не быть, но будет лишний JOIN в хорошем случае и SELECT в плохом.

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

Рекомендации проектирования для REST API

Итак, сначала спецификация — потом реализация. Вот правила, которым мы следуем помимо общепринятых.

1. Если из эндпоинта нужно вернуть данные, всегда оборачивайте их в объект

Эндпоинт (англ. endpoint — конечная точка) — метод вашего сервиса, предоставляемый для использования. Выполняет конкретную задачу, принимает параметры и данные, возвращает данные.

Мотивация:

  • эндпоинт проще расширять (например, добавить поля для постраничной навигации);

  • «фронту» иногда проще парсить ответ, особенно если возвращается массив;

  • субъективно проще воспринимается.

JSON (JavaScript Object Notation) — формат данных, который выглядит как объект в JavaScript, отсюда и название.

Пример:

[“a“, “b“] =>
{“chars“: [“a“, “b“]}.

2. При встраивании данных отдавать предпочтение объектам, а не плоской (одномерной) структуре полей с префиксами

Как и в предыдущем пункте — проще расширять, хотя здесь не будет несовместимых изменений. Тем не менее иметь рядом user_id и объект user: {«id»: 1, «age»: 20} — некрасиво, потому что это одни и те же данные, только расположенные в разных полях. Но если мы понимаем, что указанный ID от сущности во внешней системе, или мы уверены, что обогащение данными будет происходить не в этом запросе, тогда логично оставить поле в плоской структуре.

Пример:

{“book“: {“id“: 123, “author_id“: 1}} =>
{“book“: {“id“: 123, “author“: {“id“: 1}}}

3. Уделить больше внимания ошибкам. Коды ошибок (не http) явно указывать в описании

Ошибки бизнес-логики плохо сопоставляются с http-кодами. Особенно код 400: в эту ошибку обычно помещается масса кейсов — и неправильно введённый пользователем код OTP, и ошибки валидации формы, и ошибки операций, недоступных пользователю по бизнес-процессу. Без явного описания только по коду о них невозможно узнать.

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

Мы договорились использовать следующий формат ошибок:

“error”: {
    “code”: “символьный или числовой код ошибки, в идеале выносится в справочник”, 
    “message”: “понятное сообщение для пользователя в UI”,     
    “err»: “сама ошибка, подробности в зависимости от среды”
}

Возможно добавление поля с ошибками валидации дополнительно. Ошибки 401, 403, 404, 500 стоит вынести якорем и вставлять в YAML 1–4 строками.

        400:
          description: |
            Коды ошибок:
                * AuthWrongCodeError - ошибка ввода кода
                * AuthExpiredCodeError - срок действия кода
                * AuthCodeWasNotSentError - код не был получен пользователем
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"

4. Не выносите в схемы (/components/schemas) эквивалент модели в базе данных. Скорее всего, на каждый запрос будет своя схема

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

Лучше иметь отдельные схемы UserCreate, UserList и UserProfile под свои запросы.

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

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

Есть альтернативный вариант с ModelShort и ModelDetails схемами. Но велик риск, что рано или поздно потребуется схема ModelDetailsWithExtra и ей подобные, а пересечение моделей в разных запросах начнёт замедлять разработку (или выполнение запроса за счёт подготовки лишних данных).

5. Минимизируйте вложенность в URL

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

Пример: представим, есть иерархия schools — group — student. Для просмотра списка классов в школе будем иметь путь /schools/{id}/groups, а для просмотра списка учеников путь /groups/{id}/students (а не /schools/{id}/groups/{id}/students).

6. Для сериализации массива в параметрах запроса используйте style: form и explode: false

Эти параметры OpenAPI описывают понятный формат «?colors=red,green,blue». В случае кодогенерации убедитесь, что эта часть стандарта поддерживается и код адекватно работает.

          name: types
          in: query
          description: типы клуба для фильтрации
          schema:
            type: array
            items:
              type: string
              enum:
                - gym
                - online
                - studio
                - outdoor
          style: form
          explode: false
          examples:
            oneType:
              summary: Пример для одного типа
              value: [gym] # ?type=gym
            multipleTypes:
              summary: Пример для нескольких типов
              value: [gym, outdoor] # ?types=gym,outdoor

7. Для идентификаторов используйте UUID

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

Мотивация:

  • дополнительная защита от несанкционированного доступа к объектам путём инкрементального перебора идентификаторов (если не сделана проверка на владение ресурсом);

  • дополнительная помощь при ситуации объединения данных в одну таблицу из двух источников — практически нет вероятности дублирования ключа; в спецификации для строк предусмотрен специальный format: uuid.

8. Не передавайте в строке запроса персональные данные и секреты, например токены доступа

Очевидный факт, но проговорим на всякий случай.

Исключение: допустимо в случае одноразового токена.

Мотивация: эти данные попадут в логи и могут быть получены третьими лицами. Это небезопасно.

9. Определитесь со стандартом и форматом дат

Например, используйте iso-8601 YYYY-MM-DDThh:mm:ss±hh. Подумайте про таймзоны и лучше по умолчанию сразу их используйте.

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

10. Версионируйте API

И сразу используйте префикс /api/v1.

Мотивация: использование префикса тоже не требует лишних расходов, зато проще отделить новую независимую мажорную версию. Для семантического версионирования и создания чейнджлогов в автоматическом режиме хорошо использовать Bump.sh (можно ограничиться бесплатным тарифом).

Семантическое версионирование (SemVer) — это правила именования версий библиотек для обозначения ключевых изменений. Эти сведения помогают разработчикам понять, совместима ли конкретная версия с проектами, в которых использовались предыдущие версии.

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

11. Минимизируйте ответ в JSON

Мотивация: если убрать все отступы и пробелы, по сети придётся передавать меньше данных, но читать такой массив текста сложнее. Чтобы упростить чтение, можно определять формат в соответствии с заголовком Prefer return=minimal или return=representation.

12. Заполняйте примеры полей example в спецификации

Мотивация: в некоторых ситуациях при использовании мокового сервера дефолтные нули могут мешать Frontend дальше совершать какие-то действия. И они вынуждены ждать реализацию (например, 0 на счёте не даёт возможности перейти дальше по флоу). А пример с осмысленными данными (а не только 0 и string) воспринимается понятнее, и в некоторых случаях формат строки для Frontend тоже может иметь значение.

Вывод

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

Мы формируем свой стайлгайд, который будет делать спецификации более строгими и однозначными, и мы поделились основными такими моментами. Если у вас есть хорошие правила из вашей практики, обязательно пишите их в комментариях — сделаем контракты чистыми и прозрачными.

Источник: https://habr.com/ru/company/redmadrobot/blog/719222/


Интересные статьи

Интересные статьи

Давно читаю Хабр, но написать свою статью все не доходили руки. Точнее это больше ощущение, что выбранная тема может быть не интересна читателям, или у меня не получится красиво изложить мысли. Все же...
Значительная часть жизни уже давно перетекла в гаджеты, онлайн-сервисы, соцсети и мессенджеры, которые ежедневно собирают тонны персональных данных. А ими часто обмениваются компании, например, в сфер...
Построение маршрутов типичная задача, люди регулярно этим пользуются, особенно для автомобильных маршрутов, в навигаторах. Решений, для построения маршрута тоже немало, в том числе сущест...
Ранее в статье «JIRA: правила своевременного приготовления вкусного ПО. TLDR 1: границы возможностей» была  предпринята попытка унификации общих требований по применению JIRA в случае управления ...
Существует традиция, долго и дорого разрабатывать интернет-магазин. :-) Лакировать все детали, придумывать, внедрять и полировать «фишечки» и делать это все до открытия магазина.