Найти в Дзене
Записки архитектора

Про проектирование REST-спецификаций.

Оглавление

Для одного из клиентов накидал на коленке правила проектирования REST-спек в формате OpenAPI для практики системного анализа.

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

Проектирование спецификаций подразумевает создание артефакта - OpenAPI-спецификации, который может использоваться на этапах:

  • Прототипирования на этапе согласования поведения систем на уровне AV (solution-архитектура)
  • Прототипирования для согласования поведения систем на этапе ADR (системная-архитектура)
  • Прототипирование на этапе подготовки системной аналитики.
  • Реализация(разработка), используется как водящие требования.
  • Валидация системной архитектуры (на этапе QA).
  • Валидация на этапе e2e тестирования.

Дополнительные ограничения.

Следует разделять понятия контракта и спецификации:

  • Контракт - поведение информационной системы или её компонента по отношению к внешнему потребителю/потребителям.
  • Спецификация - статическое описание интерфейса, описывающее входные параметры и возможные варианты ответа, без указания специфичности ответа в зависимости от входных параметров, последовательности вызовов в сценарии, зависимых сервисов и так далее.

Несмотря на то, что спецификация не дает полного понимания о поведении системы,  OpenAPI-Спецификация является документом, отвечающим на вопросы:

  • Какими агрегатами и данными вообще оперирует система или компонент.
  • Какие ограничения есть на уровне входящих параметров у того или иного интерфейса.

RESTFull-API (интерфейсы прикладного программирования передачи репрезентативного состояния)

RESTFull-APi - набор принципов и рекомендаций, которые используются для обмена компонентов информационных систем.

REST-интерфейс подразумевает реализацию на сервисе-провайдере интерфейса, доступного по протоколу HTTP(HTTPS).

REST FULL подразумевает набор принципов, но не требований, ознакомится с описанием можно по ссылке: https://yandex.cloud/ru/docs/glossary/rest-api?utm_referrer=https%3A%2F%2Fyandex.ru%2F, нарушение принципов на уровне реализации возможно, но ведет к созданию рисков в информационном взаимодействии, управлении ресурсами и процессах эксплуатации.

Описание REST спецификации подразумевает наличие документа (спецификации), который однозначно интерпретируется всеми участниками процесса разработки и эксплуатации. Например, большая часть аппоинтеров к системам сбора логов для REST интерфейса, подразумевают запись лога в формате: дата + метод + содержимое, включая заголовки + ответ. корректное проектирование интерфейса, значительно ускоряет понимание сути ошибок или поведения сервисов на этапе разбора инцедентов.

Более строгие ограничения накладывает HTTP протокол, чье описание выходит за рамки данного документа. Но отдельно хотелось бы указать важный аспект при проектировании REST-интерфейсов, время ответа для HTTP запроса ограничено и оно ограничено веб или aplication-сервером на котором запущено приложение - сервис-провайдер.

Инструменты и формат проектирования.

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

Документация по правилам использования конструкций в описании спецификации содержится: https://swagger.io/specification/v3/ (доступно только через VPN)

Дополнительно. OpenApi спецификация, может использоваться для:

  • генерации кода, при этом генерируется шаблон сервиса, разработчику нужно наполнить его бизнес-логикой.
  • генерации запроса с pay-load, что может использоваться как аналитиками, так и в процессах QA.
  • контрактного и UNIT-тестирования.
  • создании mock-заглушек.

YAML

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

Пример хорошей YAML спецификации:

Пример

Для продвинутых пользователей документа

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

Базовые правила.

Интерфейс не хранит состояние.

Необходимо помнить, что идеологически, REST интерфейс не хранит состояние.

Что это значит?

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

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

Хранение сессий возможно, но с адекватной аргументацией и не в пределах REST-спецификации.

Так же, возможна реализация паттерна длинного запроса, когда сервис может не ответить на запрос в оп

Интерфейс не реализует вызов удаленных функций.

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

Примера возможно два:

  1. Вы явно указываете в значении параметра указывается имя функции и типы параметров. Аналогия - перебрасывание вызова СМС шлюза.
  2. Интерпретация параметров метода зависит от значения другого параметра. То есть, у вас есть атрибут в спецификации, который указывает на тип или сущность переданный в другой атрибут. Например: есть параметр type, если он delivery, то в параметре payLoad у нас id доставки, если type = order, то payLoad у нас содержит заказ.

Выбор корректных методов.

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

Для корректного чтения спецификации, необходимо корректно указывать тип реализуемого метода:

Типы методов HTTP:

GET

Метод GET запрашивает представление ресурса. Запросы с использованием этого метода могут только извлекать данные.

HEAD

HEAD запрашивает ресурс так же, как и метод GET, но без тела ответа.

POST

POST используется для отправки сущностей к определённому ресурсу. Часто вызывает изменение состояния или какие-то побочные эффекты на сервере.

PUT

PUT заменяет все текущие представления ресурса данными запроса.

DELETE

DELETE удаляет указанный ресурс.

CONNECT

CONNECT устанавливает "туннель" к серверу, определённому по ресурсу.

OPTIONS

OPTIONS используется для описания параметров соединения с ресурсом.

TRACE

TRACE выполняет вызов возвращаемого тестового сообщения с ресурса.

PATCH

PATCH используется для частичного изменения ресурса.

В общем случае. правилом хорошего тона при проектировании является использование методов: GET, POST, PUT, DELETE, PATCH

Полезные материалы

Спецификация на методы HTTP 1.1: https://datatracker.ietf.org/doc/html/rfc7231#section-4

Иерархия методов.

Структура пути REST-запроса подразумевает иерархию сущностей разделенных слэшем "/".

Правила формирования пути до метода:

/entity/subEntity/action

Где:

  • entity - сущность над которой производится действие.
  • subEntity - сущность входящая в базовую сущность.
  • action - действие которое мы собираемся совершить.

Хороший пример стиля разработки спецификации, является включение в path метода параметров.

Например:

/schools/{id}/groups

описывает метод для иерархии: schools - groups - student вернет список групп для школы с указанным ID

а метод:

/groups/{id}/students

Вернет количество студентов в указанной группе.

При этом, излишняя унификация методов, не только усложняет реализацию но и делает метод не читаемым по семантике, например:

/schools/{id}/groups/{id}/students

Является плохим примером

ВАЖНО!

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

Плохой пример:

/orderHistory/callACourier

Почему плохой? А мы точно вызываем курьера для истории заказов?

Хороший пример:

/orderHistory/{Order}/callACourier

Для продвинутых пользователей

Возможна аналогия с вложенностью классов в Java или Go

Именование методов.

  • Метод должен явно указывать на действие, которое он должен совершить.
  • Метод не должен содержать тип метода, например метод с типом GET не должен именоваться getOrder.
  • Для именования методов, да и параметров тоже, допустимо использовать только CammelCase

Кроме того, в любых именуемых сущностях спецификации, стоит избегать глаголов.

Передача данных в методах без BODY.

Есть методы, такие как GET и DELETE, у которых не должно быть BODY, а все параметры передаются в query.

Нужно помнить, что передача данных в запросе таких методов является не безопасной - содержит больше возможностей для утечки из-за снифинга, например. Стоит избегать передачи чувствительных данных в параметрах таких методов.

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

Ограничение использования метода POST.

В том случае, если вы не знаете, какой метод должен быть реализован, или выбор нужного метода противоречит правилам описанным в этом документе, допустимо использование метода POST.

Но нужно помнить, что использование POST в каждом удобном случае:

  • Снимает ограничения на осмысленность реализуемой методом функции.
  • Делает спецификацию плохочитаемой.

Ограничение длинны строки в query.

Для передаваемых в query параметрах есть ограничение на количество символов строки запроса. В RFC 2616 (Hypertext Transfer Protocol — HTTP/1.1) ограничение указывается в 255 символов, на что ориентироваться не стоит. Тем неимение, в зависимости от погоды на марсе:

  • Типа клиента/браузера
  • Типа web/aplication сервера
  • Используемого фреймворка

Количество символов может отличаться.

Стоит избегать передачи большого количества параметров и значений неопределенной длинны в строке запроса.

Примеры ограничений длинны строки запроса

  • Microsoft Edge (Browser) 81578
  • Chrome заявленное ограничение в 64 000 символов, но допускается передача до 100 000
  • Firefox (Browser) 65,536/100,000
  • Safari (Browser) 80,000 символов заявлено, возможна неограниченная длинна )
  • Opera (Browser) 190,000 символов

Версионирование API.

Наличие API  подразумевает наличие потребителей. Этот круг потребителей может быт не ограниченным или неизвестным. Возможна ситуация, в которой изменения в проектируемой спецификации может быть не обратно совместимыми с предыдущей версией API.  Это однозначно может привести к ошибкам в контракте взаимодействия между провайдером и потребителем. Кроме того, потребители могут не успеть перейти на нужную версию API, как пример - мобильные приложения.

Во избежание подобных проблем, необходимо версионировать API.

Это реализуется через  указание атрибута apiVersion в спецификации.

В общем случае, путь к методам будет выглядеть как fqdn + "/2/" где 2 - номер версии API.

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

Удаление методов.

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

В спецификации такой метод помечается атрибутом deprecated: true

По умолчанию для метода, у которого не указан явно атрибут deprecated, считается, что его значение false

Полезная ссылка

https://docs.swagger.io/spec.html#52-api-declaration:~:text=a%20MIME%20type.-,deprecated,-string

Сущности != Простой тип данных

Бывают ситуации, когда параметр метода является простым типом данных.  Предположим, что этот параметр описывает типизируемую сущность, например:

  • идентификатор пользователя в конкретном формате
  • номер доставки
  • номер телефона

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

В таких случая обязательная типизация параметра через массив "format"

Описание объектов данных в components: schema

OpenApi спецификация подразумевает три возможности описания данных:

  • Непосредственно при описании параметра в параметрах метода.
  • Вынос спецификации данных в components/schema.
  • Вынос описания в отдельную спецификацию

В случае если кроме типа данных и примера есть какая-либо дополнительная специфичность, например: форматирование, список параметров для енума и так далее. А так же в случае, если параметр встречается более чем в одном методе.

В обязательном порядке такой тип данных должен быть описан в разделе components/schemas:

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

Параметры в Query и Body не могут называться одинаково.

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

Принимаем JSON, в ответ JSON.

REST-API допускает передачу бинарных данных, в свою очередь OpenAPI поддерживает такую возможность для спецификаций через тип стринг. В зависимости от конечной реализации (используемого фреймворка) возможна типизация таких полей через format с типом base64.

Недопустимо:

  • передача бинарных данных в body без указания имени параметра.
  • возвращение данных в body-ответа без указания имени параметра.
  • передачи бинарных данных в query.

Нужно помнить, что передача бинарных данных накладывает риски на процесс разбора инцидентов, а в случае передачи в запросе, требует времени(и ресурсов) на дешифровку, если такая предполагается.

Валидный кейс:  Сохранить фото в хранилище

Не валидный кейс: упаковать таблицу данных в 1С в бинарник и ждать ответа.

Встраивание данных.

Нужно помнить, что данные в ответе с большой долей вероятности, клиентом, будут переложены в объект или многомерные структуры.

Так же стоит помнить о практике переиспользования DTO в спецификации, что дает возможность контролировать изменения, которые возникнут в результате изменения унифицированного объекта.

Не стоит избегать сложных объектов, такой ответ:

{"clientId": "123", "clientName": "Равшан"}

Хуже чем:

{"client": {"id": "123", "name": "Равшан"}}

DTO не равно модели в базе данных

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

  • Исполняемой функцией.
  • Интеграционными паттернами.

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

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

Если метод реализуется для конкретного потребителя, исключайте из DTO для ответа (да собственно и для запроса), параметры, которые не используются потребителем.  В какой-то момент вы можете обнаружить, что на той стороне могли начать использовать параметр вне согласованного сценария. Кроме того, это путает при чтении спецификации.

Обязательные параметры.

Массив required служит для указания обязательных параметров, как в DTO, так и в параметрах методов. Если предполагается обязательная передача параметра, нужно это явно указывать.

Именование DTO

DTO именуются с использованием cammelCase.

Имя объекта данных должно отражать имя сущности, которую он описывает. Использование имени метода и вообще описание ответа метода одним DTO не является хорошей практикой. В случае, когда вам необходимо реализовать кастомный ответ сервиса, хорошая практика - композиция DTO в ответе.

Правила хорошего тона.

Типизация и спецификация ошибок.

Почему правильно описывать ошибки и типизировать их:

  1. Спецификация - это документ, который описывает (не полностью) поведение сервиса.
  2. Спецификация превратится в код.

Полезно,  когда ошибки типизированы одним дата-объектом в спецификации, это дает возможность,

Клиенту:

  • Обеспечить гарантированную обработку ошибок провайдера.
  • Без обращения к прочей документации спрогнозировать поведение сервиса-провайдера или ИС.

Серверу/провайдеру:

  • Типизировать ошибки на уровне реализации.
  • Прямо переложить реакцию на сценарий взаимодействия(контракт) в код.
  • Получить возможность более широкого и приближенного к реальности тестирования на уровне UNIT-тестов.
  • Получить возможность контрактного тестирования.

Пример:

Ошибки

responses:

200:

description: Tenant successfuly created

content:

application/json:

schema:

$ref: '#/components/schemas/UserAuthData'

400:

description: |

- code: 10000

name: InvalidRequestFault

message: Invalid input request

- code: 20200

name: AuthServerNotFoundFault

message: Auth server not found

- code: 20201

name: WrongLoginOrPasswordFault

message: Wrong login or password

- code: 20202

name: AuthServerNotConfiguredByUser

message: Auth service not configured by user

500:

description: |

- code: 5050

name: DefaultError

message: Undefined error

default: true

content:

application/json:

schema:

$ref: '#/components/schemas/CommonFault'

В примере выше

200 - ответ с pay-load

400 - бизнес-ошибки

500 - внутренняя ошибка сервиса, фактически, проброшенная через try ошибка.

Данный подход требует значительных затрат на первичном этапе реализации.

HTTP-заголовки ответа Cache-Control, Expires и ETag

Более подробно про кэш в статье Стратегия кэширования.

Cache-Control, Expires и ETag — это заголовки HTTP-ответов, которые используются для управления поведением кэширования. 

Cache-Control содержит одну или несколько директив, разделённых запятыми. Эти директивы определяют, кэшируется ли ответ, и если да, то кем и как долго. Например, если установить значение Cache-Control в заголовке ответа API на max-age=60, браузер будет хранить кэш в течение шестидесяти секунд. 

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

ETag — это токен, который генерируется сервером на основе содержимого ресурса и позволяет однозначно идентифицировать его состояние. Если ресурс по данному URL-адресу изменяется, сервер создаёт новый токен Etag. Сравнение старого и нового токена от сервера помогает определить, являются ли два ресурса одинаковыми и нужно ли обновлять кэш на клиенте. 

Как минимум заголовками Cache-Control и Expires пренебрегать не стоит для чего они могут пригодится для провайдера:

  • Однозначно при реализации DRP или разборе инцидентов.
  • Отладке приложения.
  • Предиктивного определения валидности поведения кэша. Возможна и очень частая ситуация, когда по какой-либо причине, например, в связи с нарушением сетевой связанности или понижения перфоманса источника данных, нарушается процесс обновления кэша. Этот заголовок дает возможность на уровне ML над логами определить возможные проблемы в поведении сервиса-провайдера.

На уровне клиента:

  • Дает возможность понять границы нормального поведения провайдера на основе спецификации.
  • Реализовать валидацию валидности поведения провайдера, для обеспечения собственной отказоустойчивости.

Отсутствие конструкций поощряющих неопределенное поведение.

fun

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

В стандарте OpenAPI содержатся конструкции:

  • oneOf: один из, позволяет указать для параметра один из нескольких типов данных
  • anyOf: любой из описанных типов данных
  • allOf: все из описанных типов данных

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

Для опытных пользователей

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

Полезная ссылка

https://www.speakeasy.com/post/openapi-tips-oneof-allof-anyof

Все три конструкции крайне усложняют чтение и интерпретацию спецификации. А AllOf, в свою очередь, существенно усложняет проектирование спецификацию и дальнейшее её изменение.

Как можно обойти указанные ограничения?

Чаще всего, стоит переоценить поведение метода, обычно, применение подобных конструкций не оптимально и является последствием попыток упростить дата-модель. Эффект от такого упрощения описан выше.

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

Спецификация ответа 200.

Правилом хорошего тона является описание валидного ответа от сервиса. Это не только позволяет упростить проектирование клиенту, но и дает дополнительные возможности для реализации UNIT и контрактного тестирования.

Так же, правилом хорошего тона является описание и реализация HTTP ответов: 204 и 208 это сильно повышает читаемость спецификации, но допустимо для реализации только при согласовании со всеми участниками интеграции.

Description.

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

Examples.

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

Важно помнить, что OpenApi позволяет писать примеры в отвязке от объектов данных. Например, можно указать пример для кода HTTP-ответа целиком, если у ответа есть спецификация, то пример в свагер-UI выведется из примера HTTP-ответа, а не из примеров в параметрах. Если есть договоренность фиксации примеров, они должны фиксироваться в параметрах.

Вынос описания однородных данных в отдельную спецификацию .

OpenAPI дает возможность использования в нескольких спецификациях одних и тех же объектов данных такая возможность реализована через !ref.

Ограничения:

  • Swagger-editor on-line такую возможность не поддерживает
  • Необходима реализация репозитария, для локальной машине - на файловой системе, либо в git-lab, где доступна визуализация на UI гитлаба сложносвязанных спек.

Это имеет смысл когда:

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

Реализуется очень просто, нужно только начать.

HATEOAS и HAL

HATEOAS (гипермедиа как движок для состояния приложени) - паттерн, который позволяет с данными в ответ на запрос, передавать список доступных действий. Действия представлены в виде имени действия + URI метода с атрибутами в query.

Область применения HATEOAS объективно ограничивается только реализацией интерфейсов для UI, так как позволяет сконцентрировать логику на бэк-энде и упростить разработку FRONT-части приложения.

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

Так же, использование HATEOAS и HAL (язык гипертекстовых приложений) существенно усложняет отладку.

Использование тегов методов.

Массив tags в атрибутах метода. позволяет разделять методы по группам, это полезно при визуализации в инструментах использующих swagger-UI, таких как IDE, swagger-editor и backstage.

Разделение по тэгам:

  • Упрощает чтение спецификации.
  • Позволяет определить границы доменов в спецификации.

Использование тэгов позволяет определить необходимость выноса несвойственного функционала или рефакторенга API.