Разработка и управление едиными контрактами API
Привет, Хабр! Пол года назад на AnalystDays #18 я рассказывал про API-контракты, и доклад вызвал большой интерес у аудитории. Пока видео не опубликовали, решил адаптировать материал в формат статьи.
На конференции я начал с простого эксперимента. «Поднимите руку, кто пишет документацию». Рук поднялось много. «А теперь — кто уверен, что пишет идеальную документацию, от которой в восторге все кто пользуется? Честно!» Пара рук и наступила выразительная тишина, за которой последовали нервные смешки.
В каждой команде находится масса причин, почему с документацией что-то не так:
«Код самодокументируемый. Если что-то и непонятно, то это проблемы того, кто читает» — Анонимный разработчик
«Зачем документация, если есть тесты? Они и так всё проверяют» — Опытный тестировщик
«Документация устаревает быстрее, чем мы её пишем. Зачем тратить время?» — Известный системный аналитик
Каждый раз находятся какие-то оправдания. И знаете что? В каждом из этих утверждений есть доля правды. Но когда в команде нет единого источника правды о том, как должен работать API — начинаются проблемы. Сейчас расскажу, почему так происходит и как это исправить.
Классическая работа над API
Знаете, как обычно, выглядит работа над API? Системные аналитики пишут, пишут, пишут… А иногда и не пишут. Иногда создают какую-то документацию «рядом», но не само API. А потом в лучшем случае эту документацию просто берут и уносят.
Бэкенд и фронтенд разработчики, конечно, пытаются использовать эту документацию, если она есть. Но даже когда есть формальное описание API — как правило, им не пользуются. А тестировщики? Для них документация — это святое, главный артефакт. Ещё бы! Им же нужно как-то провести качественное тестирование, а не придумывать из головы, как сервис должен работать, а как не должен.
При этом вся ответственность за документацию почему-то остаётся на системных аналитиках. Разработчики очень редко вносят туда изменения — я такое видел настолько редко, что могу пересчитать случаи на пальцах одной руки. Тестировщики относятся к документации получше, но обычно ограничиваются только тем, что касается тестирования. А если что-то надо поправить? Просто говорят аналитикам на словах и отправляют их этим заниматься.
У такого подхода, конечно, куча проблем. Начнем с того, что поддержка документации превращается в кошмар. Почти всегда возникает несоответствие между тем, что ожидалось, и тем, как это реализовали. А самое главное — безумно увеличивается время на разработку.
Знаете, почему? Потому что в современных реалиях время каждого члена команды безумно дорогое. А мы вместо разработки продукта постоянно выясняем отношения: кто здесь что хотел указать, что это вообще значит, почему тут не указано, а там не сделано… И вот эти бесконечные коммуникации съедают время, которое должно было пойти на разработку продукта и ценности пользователю.
И вот тут на сцену выходят контракты. Они как раз нацелены на то, чтобы исправить эти проблемы, как минимум в части API. Благодаря им все участники команды могут работать независимо друг от друга. Да, есть небольшой люфт в начале, когда системные аналитики как первое звено разрабатывают контракт. Но зато потом все участники чётко понимают требования: что нужно разработать, в каком виде, как называется, кто куда что передает и что получает.
А ещё появляется возможность автоматизации — мы можем генерировать код, генерировать моки. Тестировщики вообще могут написать тесты ещё до того, как начнется разработка. Представляете, насколько это сокращает общее время работы?
И прежде чем мы нырнем в разные виды API и их контракты, давайте определимся с терминологией, чтобы говорить на одном языке:
Контракт API представляет собой формальное описание API, которое может включать спецификацию поведения, форматы запросов/ответов и технические детали для интеграции
Документация API включает в себя контракт API, но также может содержать руководства, примеры использования и другую информацию, полезную для разработчиков
Спецификация API — технический документ, описывающий контракт API в структурированной форме, часто используя такие стандарты как OpenAPI
Виды API и особенности их контрактов
SOAP
SOAP часто называют динозавром мира API, но знаете что? Этот «динозавр» до сих пор отлично справляется со своими задачами в корпоративном мире. Его главное преимущество — строгие контракты через WSDL. Но есть и существенный недостаток — необходимость описывать всё в XML.
Затраты по времени на борьбу с XML-схемами, чем на саму бизнес-логику — это реальность. Смотрите сами:
И это я еще показал сокращенный вариант! В жизни такой контракт может занимать сотни строк. Зато у 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, а реально работать: авторизоваться, отправлять запросы и т.п.
Документация в Swagger
В Postman выглядит похоже
Документация в Postman
При работе с OpenAPI есть и определенные нюансы. Часто используют спецификацию JSON: API — не целиком, а отдельные части для пагинации, мета-информации, разделения основных данных сущности от дополнительных.
И тут возникает интересный момент — OpenAPI позволяет использовать эту спецификацию, но когда вы генерируете документацию… Приходится делать 10 кликов, чтобы добраться до описания какого-нибудь атрибута!
JSON: API в OpenAPI
Для сравнения: с плоской структурой всё гораздо красивее и понятнее.
Плоская структуру в OpenAPI
В версии 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. Это способствует гармонии между проектированием и реализацией.
В команде наступает мир, счастье и полное понимание друг друга :)
Если остались вопросы или хотите обсудить тему подробнее — пишите в комментариях и подписывайтесь на мой канал в телеграмме.