Привет, Хабр! Пол года назад на AnalystDays #18 я рассказывал про API-контракты, и доклад вызвал большой интерес у аудитории. Пока видео не опубликовали, решил адаптировать материал в формат статьи.
На конференции я начал с простого эксперимента. «Поднимите руку, кто пишет документацию». Рук поднялось много. «А теперь — кто уверен, что пишет идеальную документацию, от которой в восторге все кто пользуется? Честно!» Пара рук и наступила выразительная тишина, за которой последовали нервные смешки.
В каждой команде находится масса причин, почему с документацией что-то не так:
«Код самодокументируемый. Если что-то и непонятно, то это проблемы того, кто читает» — Анонимный разработчик
«Зачем документация, если есть тесты? Они и так всё проверяют» — Опытный тестировщик
«Документация устаревает быстрее, чем мы её пишем. Зачем тратить время?» — Известный системный аналитик
Каждый раз находятся какие-то оправдания. И знаете что? В каждом из этих утверждений есть доля правды. Но когда в команде нет единого источника правды о том, как должен работать API — начинаются проблемы. Сейчас расскажу, почему так происходит и как это исправить.
Классическая работа над API
Знаете, как обычно, выглядит работа над API? Системные аналитики пишут, пишут, пишут… А иногда и не пишут. Иногда создают какую-то документацию «рядом», но не само API. А потом в лучшем случае эту документацию просто берут и уносят.
Бэкенд и фронтенд разработчики, конечно, пытаются использовать эту документацию, если она есть. Но даже когда есть формальное описание API – как правило, им не пользуются. А тестировщики? Для них документация – это святое, главный артефакт. Ещё бы! Им же нужно как-то провести качественное тестирование, а не придумывать из головы, как сервис должен работать, а как не должен.
При этом вся ответственность за документацию почему-то остаётся на системных аналитиках. Разработчики очень редко вносят туда изменения – я такое видел настолько редко, что могу пересчитать случаи на пальцах одной руки. Тестировщики относятся к документации получше, но обычно ограничиваются только тем, что касается тестирования. А если что-то надо поправить? Просто говорят аналитикам на словах и отправляют их этим заниматься.
У такого подхода, конечно, куча проблем. Начнем с того, что поддержка документации превращается в кошмар. Почти всегда возникает несоответствие между тем, что ожидалось, и тем, как это реализовали. А самое главное – безумно увеличивается время на разработку.
Знаете, почему? Потому что в современных реалиях время каждого члена команды безумно дорогое. А мы вместо разработки продукта постоянно выясняем отношения: кто здесь что хотел указать, что это вообще значит, почему тут не указано, а там не сделано… И вот эти бесконечные коммуникации съедают время, которое должно было пойти на разработку продукта и ценности пользователю.
И вот тут на сцену выходят контракты. Они как раз нацелены на то, чтобы исправить эти проблемы, как минимум в части API. Благодаря им все участники команды могут работать независимо друг от друга. Да, есть небольшой люфт в начале, когда системные аналитики как первое звено разрабатывают контракт. Но зато потом все участники чётко понимают требования: что нужно разработать, в каком виде, как называется, кто куда что передает и что получает.
А ещё появляется возможность автоматизации – мы можем генерировать код, генерировать моки. Тестировщики вообще могут написать тесты ещё до того, как начнется разработка. Представляете, насколько это сокращает общее время работы?
И прежде чем мы нырнем в разные виды API и их контракты, давайте определимся с терминологией, чтобы говорить на одном языке:
-
Контракт API представляет собой формальное описание API, которое может включать спецификацию поведения, форматы запросов/ответов и технические детали для интеграции
-
Документация API включает в себя контракт API, но также может содержать руководства, примеры использования и другую информацию, полезную для разработчиков
-
Спецификация API — технический документ, описывающий контракт API в структурированной форме, часто используя такие стандарты как OpenAPI
Виды API и особенности их контрактов
SOAP
SOAP часто называют динозавром мира API, но знаете что? Этот «динозавр» до сих пор отлично справляется со своими задачами в корпоративном мире. Его главное преимущество — строгие контракты через WSDL. Но есть и существенный недостаток — необходимость описывать всё в XML.
Затраты по времени на борьбу с XML-схемами, чем на саму бизнес-логику — это реальность. Смотрите сами:
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/petstore.wsdl"> <wsdl:types> <!-- Определения типов --> </wsdl:types> <wsdl:message name="GetPetRequest"> <!-- Сообщение запроса --> </wsdl:message> <wsdl:message name="GetPetResponse"> <!-- Сообщение ответа --> </wsdl:message> <wsdl:portType name="PetStorePortType"> <!-- Операции сервиса --> </wsdl:portType> <wsdl:binding name="PetStoreBinding" type="tns:PetStorePortType"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <!-- Привязка операций --> </wsdl:binding> </wsdl:definitions>
И это я еще показал сокращенный вариант! В жизни такой контракт может занимать сотни строк. Зато у SOAP есть неоспоримое преимущество — если вы правильно описали контракт, то можете быть уверены, что все участники процесса будут работать строго по документации. Никаких «я тут решил немного по-своему сделать» — или соответствуешь контракту, или твой код просто не заработает.
Это особенно ценно в корпоративной разработке, где важна надежность и формальное соответствие требованиям. Может быть, именно поэтому SOAP до сих пор активно используется в банках и крупных предприятиях, несмотря на все его многословность.
GraphQL
GraphQL очень любят мобильные разработчики и фронтендеры, и я их прекрасно понимаю. Пришел, увидел нужные данные, взял ровно то, что нужно.
Главное преимущество GraphQL в том, что на клиентской стороне разработчики могут получить те данные, которые им нужны в данный момент времени. Звучит прекрасно, правда? Но есть один нюанс — это преимущество одновременно является и недостатком, когда дело доходит до документации.
Представьте ситуацию: бэкенд-разработчики описали модель, а те, кто работает на клиенте, могут получать данные в любом виде. Казалось бы, мечта! Но потом приходят системные аналитики и спрашивают: «А как нам это всё задокументировать?» И тут начинается самое интересное – нам нужно либо описывать все возможные комбинации, либо ждать, пока клиентские разработчики закончат работу и скажут, что конкретно они используют.
Для описания контрактов (наверное больше подходит слово «моделей») GraphQL использует свой язык — GraphQL SDL (Schema Definition Language). Выглядит он довольно просто и понятно:
type Query { pet(id: ID!): Pet } type Pet { id: ID! name: String! type: String! }
И это действительно удобно — пока мы говорим о структуре данных. Но как только доходит дело до документирования реального использования API, начинаются те самые сложности с бесконечными вариациями запросов.
gRPC
gRPC – это как Ferrari среди API. Быстрый, современный, но не везде проедешь. Он работает поверх HTTP/2, использует Protocol Buffers, и это всё звучит очень круто… пока вы не попытаетесь использовать его в браузере.
Я помню проект, где команда была в полном восторге от gRPC, пока не столкнулась с реальностью браузерных ограничений. Пришлось добавлять прослойку gRPC-Web, и оптимизация производительности превратилась в квест по настройке прокси.
Но вот с контрактами тут всё очень четко благодаря Protocol Buffers. Смотрите, как просто и понятно:
service PetStore { rpc GetPetById(PetRequest) returns (Pet) {} } message Pet { int64 id = 1; string name = 2; string type = 3; }
Никакой лишней шелухи — только структура и типы данных. И главное — из этого описания можно сгенерировать код для любого поддерживаемого языка. Красота! Но пока лучше использовать для взаимодействия между сервисами.
WebSocket
WebSocket очень прост — установили соединение один раз, и можно общаться в обе стороны сколько угодно. Идеально для чатов, игр, биржевых котировок — везде, где нужны данные в реальном времени.
Раньше с документированием WebSocket был полный хаос — кто в Excel писал, кто в Wiki (кое-как), кто в задачах. Но потом появился AsyncAPI, и всё встало на свои места. Тот же YAML, который мы полюбили в OpenAPI, но заточенный под асинхронное взаимодействие. И главное — он одинаково хорошо описывает как WebSocket, так и очереди сообщений вроде Kafka.
В качестве примера AsyncAPI можете взять репозиторий, который я делал для выступления на AnalystDays #19 (скоро выйдет отдельная статья) — https://github.com/nemirlev/async-api-example
WebHook
WebHook — это по сути обычный HTTP POST, который сервер отправляет на указанный URL, когда что-то интересное случилось. Простая идея, но документировать такое взаимодействие раньше было непросто.
К счастью, тот же AsyncAPI отлично справляется и с этой задачей. Описываете события, их структуру, точки доставки — и все участники процесса точно понимают, чего ожидать. А если у вас в проекте уже используется AsyncAPI для WebSocket — вы получаете единый стиль документации для всего асинхронного взаимодействия.
REST
REST — это очень гибкий архитектурный подход, который устанавливает определенные ограничения для API. И знаете что? Именно эта гибкость является одновременно его преимуществом и недостатком.
То, как мы разработаем REST API, полностью в наших руках. Мы можем сделать все запросы просто через POST для получения, добавления и удаления данных. Сам подход нам этого не запрещает. Или можем красиво разделить всё на CRUD-операции, сделав API удобным и понятным. REST нам не указывает, как именно это делать — он просто задает рамки.
Именно из-за этой свободы появилось множество спецификаций для описания REST API. Давайте посмотрим на основные:
RAML — один из старейших и при этом до сих пор популярных форматов. В 2016 году у него убрали поддержку, но знаете что интересно? Его используют до сих пор. Почему? Потому что YAML — это понятно, удобно писать быстро, понятно и удобно читать. Существует огромное количество инструментов для кодогенерации. И он настолько хорош, что больших изменений в него не требуется — с ним действительно удобно и комфортно работать.
API Blueprint — это более свежий и необычный подход. Весь контракт описывается с помощью Markdown. Я бы его сравнил с WSDL по накладным расходам на написание — он больше подойдет тем, кто привык всю документацию писать в Markdown. Пока выходила эта статья, буквально 3 недели назад я заметил, что репозиторий API Blueprint был перенесен в архив и больше не поддерживается. К сожалению, официальной информации о причинах прекращения поддержки API Blueprint я пока найти не смог. Если кто-то знает подробности об этом, буду рад увидеть ваши комментарии.
OpenAPI (который многие до сих пор называют Swagger) — это фактически стандарт для разработки контрактов REST API. Он позволяет описывать API как в XML, JSON, так и в YAML, хотя JSON я, честно говоря, очень редко видел в реальной работе. YAML гораздо удобнее — Enter, таб, и поехали и чаще всего используется.
На OpenAPI я предлагаю остановиться подробнее, потому что это сейчас основная спецификация для работы с REST API контрактами.
OpenAPI
Знаете, что меня всегда удивляло? Когда на собеседованиях спрашиваешь системных аналитиков: «Вы работали с OpenAPI?» — «Нет». — «А со Swagger?» — «Да!». Это наследие тех времен, когда инструмент назывался Swagger, затем его отдали в open source, и теперь Swagger — это целая API-платформа, которая позволяет генерировать документацию, создавать моки, управлять версиями и многое другое.
Из OpenAPI-спецификации мы можем получить человекочитаемую, динамическую документацию. В ней можно не просто читать описание API, а реально работать: авторизоваться, отправлять запросы и т.п.
В Postman выглядит похоже
При работе с OpenAPI есть и определенные нюансы. Часто используют спецификацию JSON:API — не целиком, а отдельные части для пагинации, мета-информации, разделения основных данных сущности от дополнительных.
И тут возникает интересный момент — OpenAPI позволяет использовать эту спецификацию, но когда вы генерируете документацию… Приходится делать 10 кликов, чтобы добраться до описания какого-нибудь атрибута!
Для сравнения: с плоской структурой всё гораздо красивее и понятнее.
В версии 4, которую обещают выпустить скоро, многие из этих неудобств должны исправить. Это проблема не мешает создавать качественные контракты и документацию, просто добавляют небольшие неудобства в работе. Ну, и раз в целом перешел на рекомендации, поделюсь немножко еще опытом.
Рекомендации
Первое, что обычно делают — берут один файл и описывают в нем все 100 ручек API. В OpenAPI есть импорт, им стоит пользоваться. Например:
# api-specifications/microservice-1/v1/openapi.yaml openapi: 3.0.0 info: title: Microservice 1 API version: 1.0.0 servers: - url: https://api.example.com/v1 paths: /users: post: summary: Create a user requestBody: content: application/json: schema: $ref: './models/user.yaml#/User' responses: '201': description: Successful response content: application/json: schema: $ref: './models/user.yaml#/User'
# api-specifications/microservice-1/v1/models/user.yaml User: type: object properties: id: type: string name: type: string email: type: string
Сразу задумайтесь над тем, как API будет лежать в проекте, как его будут читать, где хранить, как делить файлы и модели. Начните с четкого разделения по микросервисам и сервисам. Даже если у вас монолит — можно спокойно разделить по логическим сервисам внутри него.
Так же нужно учесть версионирование. Не все его используют, и иногда это больно, а иногда нет. Но когда пишете контракт — используйте хотя бы версию 1. Потому что когда «стрельнет», и вам придется делать версию 2, будет гораздо проще, даже если вы с первой версией прожили 5 лет.
Вот как это может выглядеть:
api-specifications/ ├── microservice-1/ │ ├── v1/ │ │ ├── openapi.yaml # Основная спецификация │ │ └── models/ # Модели данных │ └── v2/ ├── microservice-2/ └── shared/ # Общие компоненты ├── responses.yaml # Общие ответы └── parameters.yaml # Общие параметры
Язык документации. Тут я всегда придерживаюсь простого правила: если компания работает в России и продукт для России, то пишите на русском. Особенно если проект не планируется выкладывать на GitHub. Почему? Потому что найти разработчиков, системных аналитиков и тестировщиков, которые прекрасно говорят на английском — дорого, да и просто сложно.
Есть и другая проблема. Думаю вам встречалось, когда человек вроде знает английский, но когда надо что-то описать, начинает стесняться возможных ошибок и в итоге пишет пару слов вместо нормального описания. Современные инструменты позволяют писать на любом языке — используйте это преимущество.
Описания везде. В OpenAPI можно добавлять описания в разных местах: в параметрах, путях, схемах, атрибутах. Используйте их все! Даже если это просто ID пользователя — опишите. Когда в одном месте есть описание, а в другом нет, при чтении документации начинаешь спотыкаться. А когда описания есть везде — появляется определенный такт, и читать становится гораздо удобнее.
paths: /pets/{petId}: get: description: "Получение информации о питомце по его идентификатору" parameters: - name: petId in: path description: "Уникальный идентификатор питомца в системе" required: true schema: type: string responses: '200': description: "Успешное получение информации о питомце" content: application/json: schema: $ref: '#/components/schemas/Pet' components: schemas: Pet: description: "Информация о питомце" type: object properties: id: description: "Уникальный идентификатор питомца" type: string name: description: "Кличка питомца" type: string
Секция info. Ею часто пренебрегают, а зря. Опишите, для чего это API, кто будет использовать, укажите все серверы (dev, test, prod). Это потом упростит кодогенерацию и работу. Версию тоже указывайте осмысленно — даже если это просто внутренняя версия для аналитиков, используйте две последние цифры. Поменяли описание — увеличили последнюю цифру, серьезное изменение — вторую.
openapi: '3.0.0' info: title: "API Сервиса управления питомцами" description: | API для работы с данными о питомцах в нашей системе. Позволяет создавать, получать, обновлять и удалять информацию о питомцах. Основные возможности: * Управление профилями питомцев * Поиск питомцев по различным критериям * Работа с медицинскими картами version: '1.0.3' servers: - url: 'https://api.petstore.dev.example.com' description: 'Development сервер' - url: 'https://api.petstore.test.example.com' description: 'Test сервер' - url: 'https://api.petstore.example.com' description: 'Production сервер'
Примеры. Если не указать примеры, то при генерации, например коллекций в Postman, вместо нормальных значений будет просто «string». Это жутко неудобно! Потратьте немного времени на реальные примеры данных — облегчит жизнь даже вам спустя пол года.
components: schemas: Pet: type: object properties: id: type: string example: "pet-123e4567-e89b" name: type: string example: "Барсик" type: type: string example: "cat" enum: ["cat", "dog", "hamster"] status: type: string example: "available" description: "Статус питомца в системе" example: id: "pet-123e4567-e89b" name: "Барсик" type: "cat" status: "available"
Авторизация. Частая история — схема есть, контракт есть, всё описали, но авторизация где-то в Wiki на отдельной странице. А потом там что-то меняется, а в API не отражается. Включайте авторизацию прямо в контракт:
components: securitySchemes: OAuth2: type: oauth2 flows: authorizationCode: scopes: read: "Доступ на чтение" write: "Доступ на запись"
И потом в конкретных путях можете указывать необходимые права. Это не только поможет при генерации кода, но и сделает документацию более понятной.
components: securitySchemes: OAuth2: type: oauth2 description: "OAuth 2.0 авторизация" flows: authorizationCode: authorizationUrl: https://auth.example.com/authorize tokenUrl: https://auth.example.com/token scopes: pets:read: "Чтение информации о питомцах" pets:write: "Создание и изменение информации о питомцах" pets:delete: "Удаление информации о питомцах" paths: /pets: get: security: - OAuth2: ['pets:read'] description: "Получение списка питомцев" # ...остальное описание метода post: security: - OAuth2: ['pets:write'] description: "Создание нового питомца" # ...остальное описание метода /pets/{petId}: delete: security: - OAuth2: ['pets:delete'] description: "Удаление питомца" # ...остальное описание метода
Процессы
Внедрение контрактов API может показаться простым — написали спецификацию, внедрили инструменты, все должно заработать. Но в реальной командной работе этого недостаточно.
Часто возникает ситуация, когда мы начинаем использовать контракты, но все равно сталкиваемся с проблемами. Представим типичный сценарий:
Аналитик сделал схему API, а разработчик по ходу реализации решил, что она сделана «не очень» и внес свои изменения, но в код. Фронтенд-команда в свою очередь тоже взяла схему у аналитика, но в итоге пошла к бэкенду разбираться, так как их реализация «не работала». Когда подключились тестировщики, они сгенерировали 50 дефектов, основываясь на схеме аналитика, но выяснилось, что у разработчиков своя версия и «источник истины» отсутствует.
Это типичная ситуация, когда формальное внедрение контрактов не решает проблем. Нужно менять сам процесс работы команды. Например:
-
Аналитик создает документацию с описанием контракта API.
-
Лиды направлений (тестирования, бэкенда, фронтенда) верифицируют эту схему, чтобы убедиться, что она удовлетворяет всем требованиям.
-
После верификации бэкенд и фронтенд параллельно приступают к реализации, а тестировщики пишут автоматизированные тесты.
-
Если в ходе работы необходимо внести изменения в контракт, они вносятся и снова валидируются всеми заинтересованными сторонами.
-
После реализации достаточно просто прогнать автоматизированные тесты, чтобы убедиться в работоспособности.
Инструменты
Для командной работы, есть много инструментов:
-
Git. Отдельный репозиторий, в котором лежит API. Участники для кодогенерации/документирование спокойно подключают к сервисам в виде git submodule. Все валидация на этапе после того, как аналитики разработали и при изменениях по ходу работы кем-то из участников, осуществляется с помощью MR/PR. Можно настроить правила для репы, чтобы каждый раз ручками не звать людей. Из плюсов такого подхода: всем понятный инструмент и легко начать.
-
Postman — здесь сразу и работа со схемой в команде, автодополнение, валидация, генерация мок-серверов и тестов, удобные коллекции, просмотр документации по аналогии со Swagger и аналогами и даже мониторинг. Единственное неудобство, что работа с текущей версией API, одновременно несколькими людьми — изменения каждого человека фиксируется только после фиксации версии. То есть нет DIFF всех участников в черновике, что в целом можно полечить договоренностями, в остальном очень удобно.
-
Git + Postman. Самый идеальный вариант, но в Postman есть интеграция только с GitHub и GitLab, если репа на своем домене, уже не воспользуешься. Обещают скоро исправить. Фактически преимущество первых двух подходов и самый идеальный вариант. У вас вся работа с API, кроме имплементации, в одном месте (даже мониторинг API на проде можно подрубить).
-
Insomnia — легковесный аналог Postman, можно вести документацию, делать запросы, писать тесты, простая кодогенерация.
-
Swagger — тут, думаю, пояснений не надо, основное — работа со схемой. В целом большая часть возможностей есть сейчас в любой IDE как минимум в виде плагинов.
-
Redoc, Apicurio Studio, Stoplight Studio, Speccy и ооочень много других…
Думаю каждый для себя может найти удобный инструмент.
Итоги
Внедрение контрактов API приносит целый ряд важных преимуществ. Прежде всего, это четкое определение интерфейсов и ожидаемого поведения API. Контракт становится единым источником правды, к которому могут обращаться все участники процесса — от аналитиков до тестировщиков. Это позволяет избежать недопонимания и ошибок в интеграции.
Кроме того, контракты упрощают процесс разработки и тестирования. На их основе можно автоматизировать создание кода, клиентских библиотек и тестов. Это ускоряет реализацию и повышает качество конечного продукта.
Важно и то, что контракты способствуют улучшению совместимости и масштабируемости систем. Стандартизация подходов к описанию API помогает обеспечить их обратную совместимость при дальнейшей эволюции.
Наконец, контракт становится гарантией актуальности документации, которая всегда отражает текущее состояние API. Это способствует гармонии между проектированием и реализацией.
В команде наступает мир, счастье и полное понимание друг друга 🙂
Если остались вопросы или хотите обсудить тему подробнее – пишите в комментариях и подписывайтесь на мой канал в телеграмме.
ссылка на оригинал статьи https://habr.com/ru/articles/860960/
Добавить комментарий