Разработка и управление едиными контрактами 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/


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

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

Добавляем функционал быстрых ответов на любом тарифе Битрикс24Как добавить одну из самых полезных функций для чатов открытых линий для любого коммерческого тарифа? Все очень просто, нужно лишь..... ...
Рано или поздно, каждый пэхапешник, пишущий на битриксе, начинает задумываться о том, как бы его улучшить, чтобы и всякие стандарты можно было соблюдать, и современные инструменты разработки использов...
Настройка любой площадки для CMS — это рутинный процесс, который должен быть доведен до автоматизма в каждой уважающей себя компании. А потому частенько воспринимается, как восход солнца — это происхо...
Свежая подборка новостей и материаловИнтересное в этом выпуске веб-браузер, мониторинг почтовых служб, сканер уязвимостей, зашифрованная файловая системаПриятного чтения!...
Компания Facebook расширяет свой европейский дата-центр в городе Оденс (Дания) на 30 тыс. м². Расширение инфраструктуры обойдется компании приблизительно в 10 млрд датских крон ($...