Разработка и управление едиными контрактами 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-схемами, чем на саму бизнес-логику — это реальность. Смотрите сами:

<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, а реально работать: авторизоваться, отправлять запросы и т.п.

Документация в Swagger

Документация в Swagger

В Postman выглядит похоже

Документация в Postman

Документация в Postman

При работе с OpenAPI есть и определенные нюансы. Часто используют спецификацию JSON:API — не целиком, а отдельные части для пагинации, мета-информации, разделения основных данных сущности от дополнительных.

И тут возникает интересный момент — OpenAPI позволяет использовать эту спецификацию, но когда вы генерируете документацию… Приходится делать 10 кликов, чтобы добраться до описания какого-нибудь атрибута!

JSON:API в OpenAPI

JSON:API в OpenAPI

Для сравнения: с плоской структурой всё гораздо красивее и понятнее.

Плоская структуру в 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. Это способствует гармонии между проектированием и реализацией.

В команде наступает мир, счастье и полное понимание друг друга 🙂

Если остались вопросы или хотите обсудить тему подробнее – пишите в комментариях и подписывайтесь на мой канал в телеграмме.


ссылка на оригинал статьи https://habr.com/ru/articles/860960/


Комментарии

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

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