Привет, Хабр! Меня зовут Виктория Юльская, и я старший системный аналитик в Ozon.

Я думаю, здесь найдётся много людей, которые хоть раз работали с документацией API в Confluence. Да-да, те самые километровые страницы на каждый метод — с описанием всего и вся в виде текста, таблиц, диаграмм последовательности и т. д.

Зачастую такая документация API в Confluence устаревает ровно в тот момент, как её закончили писать. После передачи задачи в разработку, как только что-то непонятно, куда все идут? Правильно, к аналитику — «А как это работает? А что это значит? А что если...?».

Ну вот же дока, там всё написано... но обычно никто не хочет читать огромную доку на метод, быстрее же спросить. И зачастую у самих аналитиков есть вопросики по актуальности этой документации (уже есть новые договорённости со встреч, комментарии в документации и т. д.).

Есть ли более эффективный способ ведения и поддержания документации API в актуальном состоянии? Давайте разбираться.

Немного вводной части

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

То, о чём мы сегодня с вами поговорим, наилучшим образом ложится на подход Specification-first / Manifest First / Design API First — называйте как хотите, но суть одна — сначала спецификация, потом код!

Эта тема достаточно холиварная и зачастую сталкивает лбами системных аналитиков и разработчиков :) Тем не менее подход не новый и давно уже показал себя как вполне себе эффективный.

Не будем сильно погружаться в сравнение подходов и как сделать правильный выбор, но в двух словах подсветим преимущества подхода Design API First:

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

  • Параллельная разработка
    Спроектировав заранее контракт API, команды тестирования, разработки клиента API и разработки сервиса, реализующего API, могут работать параллельно, что, в свою очередь, приводит к увеличению производительности команд.

  • Скорость и качество тестирования
    Спецификация API упрощает QA-специалистам создание тест-кейсов, что обеспечивает общее более высокое качество ПО. Также на основе спецификации OpenAPI можно генерировать API-тесты.

  • Кодогенерация
    Из готовой спецификации OpenAPI можно сгенерировать клиент, сервер, документацию (HTML, Confluence Wiki), ну и, как было сказано выше, — API-тесты, что позволит вам автоматизировать большую работу в рамках всего проекта и уменьшить объём рутинного кода.

  • Проектирование API аналитиком и архитектором (если таковых не имеется, то с привлечением разработчиков)
    Мы создаём API для потребителей, и аналитик, как никто другой, помнит об этом при проектировании удобного и понятного API с подробным описанием и примерами. Плюс к тому же, аналитик знает потребности пользователей, пользовательский путь и как работает система изнутри, что позволяет ему качественно описать необходимые методы и модели.

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

    • как будут взаимодействовать аналитик, архитектор и разработчик при проектировании спецификации;

    • процесс ревью спецификации;

    • как вносить изменения в спецификацию;

    • как уведомлять о готовности спецификации;

    • где хранить спецификации и куда доставлять изменения;

    • поддержка моков и многое другое.

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

Спецификация OpenAPI

Все мы хорошо знаем, что REST — это архитектурный стиль, а не конкретный стандарт. Тем не менее есть несколько спецификаций REST, которые помогают представить стандарты в виде описания REST API.

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

Наиболее используемые инструменты Swagger:

  • Swagger UI — веб-приложение, которое позволяет визуализировать спецификацию Open API в интерактивном пользовательском интерфейсе, выполнить запрос на сервер и получить ответ;

  • Swagger Codegen — генерация клиентов, серверных заглушек, SDK-пакетов и документации на основе определений спецификации Open API;

  • Swagger Core — генерация документации на основе аннотаций в существующем коде.

Нас больше всего сейчас интересует, что использовать для разработки спецификации OpenAPI, а это:

  • Swagger editor — интерфейс для создания файла документации по спецификации Open API. Не очень удобен тем, что большой файл будет грузиться достаточно долго...

  • Любая IDE с расширением для валидации и визуализации OpenAPI-спецификации (Swagger Viewer и др.).

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

Спецификацию OpenAPI мы можем писать как на JSON, так и на YAML.

С YAML работать легче, потому что не требуется расставлять скобки и запятые, т.е. документ более понятный для человека, но есть нюанс — надо внимательнее следить за расстановкой интервалов.

Далее будем рассматривать структуру спецификации OpenAPI в обоих вариантах.

Организация файлов спецификации OpenAPI

  • Для того чтобы спецификацию можно было легко и приятно поддерживать, да и в целом читать, я обычно выношу описание методов и описание моделей в отдельные файлы и ссылаюсь на нужные существующие объекты с помощью указания путей до них. Можно отдельно вынести и описание параметров, описание ошибок — но это поможет, если ваши сервисы содержат очень много повторяющихся параметров и/или ошибок.

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

  • Относитесь к спецификации как к программному коду. Иначе в конечном итоге она превратится в мусор.

Для себя я выделила следующий паттерн в организации файлов спецификации OpenAPI:

  • root_folder ← корень репозитория

    • resource_or_api_controller ← папка ресурса (если делим не по тегам в одном файле спецификации, а разносим по отдельным файлам)

      • api.yaml← файл для описания методов

      • models.yaml← файл для описания моделей

      • parameters.yaml← файл для описания параметров api-методов (опционально) 

      • errors.yaml← файл для описания ошибок, которые могут вернуть методы (опционально) 

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

Базовая структура спецификации OpenAPI

Из чего состоит спецификация OpenAPI:

  • openapi - содержит номер версии спецификации OpenAPI. 

  • info - cодержит основную информацию о вашем API: название, описание, версию, контакты разработчика спецификации и т. д.

  • servers - содержит информацию об используемых серверах.

    • указывается базовый путь, который находится перед конечной точкой.

    • можно указать несколько вариантов - для разных сред разработки

  • components - в объекте components можно хранить множество различных переиспользуемых объектов. Объект components может содержать следующее: схемы, ответы, параметры, примеры, тело запроса, заголовки, схемы безопасности и тд. При делении спецификации на два файла — api.yaml и models.yaml — нам данный блок нужен только для определения схемы безопасности, всё остальное уходит в файл models.yaml.

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

    • объявленные поля components.securitySchemes и security свидетельствуют о том, что у любого метода в этом файле должен быть установлен хедер Authorization, в нашем случае с JWT-токеном.

    • при этом у каждого метода можно определить секцию security:[], оставив её пустой, которая будет свидетельствовать о том, что для данного метода авторизация не нужна.

    • о том, как декларировать различные схемы авторизации (apiKey, http, ouath2, openIdConnect, mutualTLS), можно в подробностях почитать в официальной документации OpenAPI

  • paths - содержит доступные пути (конечные точки) и операции (методы) для API. Подробнее о заполнении данного блока рассмотрим в следующем разделе.

  • tags - в данном объекте перечисляются все теги, в которые вы будете объединять свои конечные точки (по пользовательским ролям или фичам продукта).

  • externalDocs - содержит ссылки на внешние ресурсы для получения расширенной информации.

Пример заполнения в yaml
openapi: 3.1.0
info:
  title: "Myproject API"
  description: “Description of the purpose of your service”
  version: "1.0.0"
  termsOfService: “https://myproject.ru/terms”
  contact:
    name: Ванька Петров
    email: user@gmail.com
servers:
 - url: https://dev.api.myproject.ru/api/v2
   description: Test server dev
 - url: https://stg.myproject.ru/api/v2
   description: Test server stg
 - url: https://myproject.ru/api/v2
   description: Test server prod
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
paths:
  /users:
	get:
      security:
    	- bearerAuth: []
tags:
 - name: Profile
   description: "Профиль"
 - name: Products
   description: "Товары" 
externalDocs:
  description: Find more info here
  url: https://example.com

Пример заполнения в json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Myproject API",
    "description": "Description of the purpose of your service",
    "version": "1.0.0",
    "termsOfService": "https://myproject.ru/terms",
    "contact":{
  	    "name": "Ванька Петров",
       	"email": "user@gmail.com"
    	}
  },
  "servers": [
	{
    	"url": "https://dev.api.myproject.ru/api/v2",
        "description":"Test server dev"
	},
	{
    	"url": "https://stg.myproject.ru/api/v2",
        "description":"Test server stg"
	},
	{
    	"url": "https://myproject.ru/api/v2",
        "description":"Test server prod"
	}
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "jwt"
      }
    }
  },
  "paths": {
    "/users": {
      "get": {
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    }
  },
  "tags": [
   {
     "name": "Profile",
     "description": "Профиль"
   },
   {
     "name": "Products",
     "description": "Товары"
   }
  ],
  "externalDocs":{
   "description": "Find more info here",
   "url": "https://example.com"
  }
}

Далее чуть подробнее разберёмся с ресурсами и методами. Для общего понимания — как может выглядеть ваша спецификация на один метод (в упрощённом варианте описания):

api.yaml
info:
  title: API Test
  version: '1.0'
servers:
  - url: https://api.server.test/v1
paths:
  /feedback/{id}/report:
	post:
     summary: Название метода
  	description: Описание работы метода
      security:
    	- bearerAuth: []
  	parameters:
    	- in: path
      	name: id
      	required: true
      	schema:
        	type: string
  	requestBody:
    	content:
      	application/json:
        	schema:
          	$ref: '#/components/schemas/Report'
    	required: true
  	responses:
 	   '200':
      	description: OK
      	content:
        	application/json:
          	schema:
            	$ref: "#/components/schemas/InfoMessage"
components:
  securitySchemes:
  	bearerAuth:
    	type: http
    	scheme: bearer
       bearerFormat: JWT
  schemas:
	Report:
  	type: object
  	required:
    	- id
    	- name
  	properties:
    	id:
      	type: integer
      	format: int64
    	name:
      	type: string
	InfoMessage:
  	type: object
  	required:
    	- code
    	- message
  	properties:
    	code:
      	type: integer
      	format: int32
    	message:
      	type: string         

Ресурсы и методы

Объект path содержит доступные пути (конечные точки) и операции (методы) для API. Он состоит из:

  • пути (конечной точки) — все пути в блоке path задаются относительно URL, определённых в блоке «Серверы», то есть полный URL запроса будет выглядеть так: <server-url>/path.

  • операций (методов GET, POST и т. д.), которые в свою очередь включают:

    • summary — название метода

    • description — описание работы метода. Описывайте задачу, которую решает метод или свойство

    • security: [] — определяет глобальный метод безопасности

    • parameters — параметры запроса

    • requestBody — тело запроса

    • responses — описание ответа

Есть множество других элементов, но остановимся на обязательной основе.  

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

Есть 3 типа ресурсов:

  • Документ — один объект. К примеру, одно сообщение в списке (api/messages/{id} — документ обычно вложен в коллекцию, но есть исключения).

    • в пути используются в таком случае только существительные.

    • последнее существительное в единственном числе.

  • Коллекция — множество объектов. К примеру, список сообщений (api/messages).

    • в пути используются в таком случае только существительные.

    • последнее существительное во множественном числе.

  • Контроллер — действие. К примеру, оформление заказа (/api/cart/checkout).

    • можно использовать глаголы.

    • действие должно относится к чему-то (/api/cart/checkout — checkout относится к корзине, лучше не делать просто /api/checkout — не самый лучший пример, но суть должна быть понятна).

Если вы следуете принципам REST, то в названии пути не пишем действие, о котором говорит HTTP method (create, update, delete и т. д.). Стараемся делать как можно больше документов и коллекций и как можно меньше контроллеров.

POST /courses — Создать новый курс

POST /courses/create — Создать новый курс 

Но здесь надо помнить, что ваш API может быть реализован полностью на POST, и в таком случае вполне нормально использовать действия в названии пути:

POST /courses/create — Создать новый курс

Методы

Метод

Описание

Комментарий

GET

Возвращает представление ресурса по указанному универсальному коду ресурса (URI). Текст ответного сообщения содержит сведения о запрашиваемом ресурсе.

GET-запрос может содержать тело запроса, но прокси могут просто отбрасывать тело GET-запроса.

GET-запросы по умолчанию кешируются через URI. Если параметры передаются в теле, то кеши работать не будут.

ОС может самостоятельно повторить GET-запрос.

POST

Создаёт новый ресурс по указанному URI. Текст запроса содержит сведения о новом ресурсе.  

Метод POST также можно использовать для запуска операций, не относящихся непосредственно к созданию ресурсов (для операции контроллера).

PUT

Создаёт или заменяет ресурсы по указанному URI.  

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

Лучше все-таки разделять создание (делать POST) и изменение (PUT / PATCH).

Полностью перезаписывает ресурс.

PATCH

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

Перезаписывает только определённую часть.

 

DELETE

Удаляет ресурс по указанному URI.

Не содержит тела.

Параметры пути и запроса

Параметры пути и запроса состоят из:

  • name: имя параметра.

  • in: место параметра. Возможные значения:

    • header — параметры, включённые в заголовок запроса

    • path — параметры в пределах path конечной точки перед строкой запроса

    • query — параметры в строке запроса конечной точки

    • cookie — параметры в заголовке Cookie

  • description: описание параметра.

  • required: обязательность параметра.

  • schema: схема или модель для параметра. Схема определяет структуру входных или выходных данных.

  • example: пример типа носителя. Если объект example содержит примеры, эти примеры появляются в Swagger UI, а не в содержимом объекта example.

Также параметры запроса можно выносить в models.yaml и ссылаться ($ref) на параметры из моделей. Пример:

parameters:

  - $ref: "models.yaml#/components/parameters/Param1"

Для добавления в компоненты параметров необходимо на уровне с элементом schema добавить элемент parameters и описать там все необходимые параметры.

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

Описание параметра запроса в models.yaml для того, чтобы ссылаться на него и переиспользовать без дублирующего описания.

parameters:
  filter_type:
   name: filter_type
   in: query
   description: |
 	Тип фильтра заказов пользователя:
 	- all — все заказы
 	- current — текущие
 	- done — выполненные
   schema:
 	enum: ["all", "current", "done"]
  	type: string

Параметры пути

Для того чтобы добавить параметр в путь запроса, необходимо использовать фигурный скобки {}. Обычно это используется для указания определённого элемента в коллекции. Путь может иметь несколько параметров

GET /users/{userId}:

GET /cars/{carId}/drivers/{driverId}:

Каждый параметр пути должен быть заменён фактическим значением при вызове.

Для определения параметров пути нужно использовать следующую конструкцию in: path. Необходимо также добавить required: true, чтобы указать обязательность данного параметра.

paths:
  /users/{userId}: 
    get:
      parameters:
	    - name: userId   # имя использовать такое же, как и в пути
  	      in: path
  	      description: Идентификатор пользователя
  	      required: true # обязательный параметр
  	      schema:
            type: integer
            format: int64

Параметры запроса

Параметры запроса отображаются в конце URL-адреса после знака вопроса (?). Несколько значений должны разделяться амперсандом (&).

GET /pets/findByStatus?status=available

GET /notes?offset=100&limit=50

Для определения таких параметров нужно использовать следующую конструкцию in: query.

paths:
  /notes:
    get:
      parameters:
	    - name: offset
  	      in: query
  	      description: The number of items to skip before starting to collect the result se
  	      schema:
    	    type: integer
	    - name: limit
  	      in: query
  	      description: The numbers of items to return
  	      schema:
     	    type: integer
Примеры оформления параметров запроса.
Объявление параметра запроса
Объект в качестве параметра запроса (поддерживается с OpenAPI 3)
Ссылка на модель с параметрами

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

Тело запроса

POST-, PUT- и PATCH-запросы могут иметь тело запроса.

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

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

Пример:

requestBody:
  required: true
  content:
   	application/json:
     	schema:
         	$ref: "models.yaml#/components/schemas/RecalculateOrderRequest"
Примеры оформления тела запроса.
Объявление тела запроса
Объявление тела запроса ссылкой на модель

Ответ

Описание REST-запроса обязательно должно содержать описание ответа (responses). Response задаётся HTTP-кодом ответа и данными, которые возвращаются в теле ответа и/или заголовке.

Описание ответа начинается с кода, такого как 200 или любого другого. Методы обычно возвращают один успешный код и один и более кодов ошибок. Каждый код требует описания (description) — условие, при котором код срабатывает. Если вы не можете задать определённый код, то его можно задать следующим видом: 1XX, 2XX, 3XX, 4XX, 5XX. Но таким образом в случае, если был задан код 404 и 4XX, приоритет у первого будет выше.

responses:
	'201':
  	description: Бонусы успешно списаны.
     '400':
  	description: Неверный запрос
  	content:
    	application/json:
      	schema:
        	$ref: "../common/models.yaml#/components/schemas/ResponseBadParameters"
   '401':
  	description: Несанкционированный запрос
  	content:
    	application/json:
      	schema:
        	$ref: "../common/models.yaml#/components/schemas/ResponseUnauthorized"
    	'500':
  	description: |
    	Возможные ошибки
    	* `101` — UserBlocked, пользователь был заблокирован
    	* `104` — OTPCodeInvalid, неверный OTP-код
  	content:
    	application/json:
      	schema:
         	$ref: "../common/models.yaml#/components/schemas/ErrorResponse"
	'426':
   	description: Необходимо обновить приложение

Для передачи файлов в запросе или ответе в OpenAPI 3.0 используется type: string и format: binary или format: base64.

paths:
  /report: 
    get:
      summary: Returns the report in the PDF format
      responses:
	    '200':
  	      description: A PDF file
  	      content:
    	    application/pdf:
      	      schema:
        	    type: string
         	    format: binary
Примеры оформления ответа.
Объявление ответа ссылками на модели
Объявление ответа ссылками на модели
Обычный текстовый ответ с заголовками
Обычный текстовый ответ с заголовками

Статус-коды (основные)

Код

Описание

Часто используемые коды

2xx

Операция завершилась успешно

200 OK — ответ на успешные GET, PUT, PATCH, DELETE, а также для POST, который не привёл к созданию.

201 Created — используется в методах POST и имеет тело ответа, чтобы сказать клиенту, что мы создали в итоге — как минимум получить идентификатор записи.

202 Accepted — указывает на то, что запрос принят к обработке (обработка ещё не завершена или даже не начата), и клиенту необязательно ждать завершения операции.

204 No Content — операция прошла успешно, но тело ответа не требуется (например, запрос DELETE). 

3xx

Редирект или можем пойти читать из кэша

304 Not Modified — свидетельствует о том, что данные не изменились и можно читать данные из кэша. Обычно работает с E-Tag или Cache-Control-заголовками.

4xx

Операция завершилась с ошибкой по вине клиента

Из тех, что стоит фиксировать в спецификации:

400 Bad Request — сервер не смог понять запрос из-за недействительного синтаксиса.

401 Unauthorized — пользователь не авторизован для доступа.

403 Forbidden — пользователь не имеет права на запрашиваемый ресурс.

404 Not found — запрашивается несуществующий ресурс.

426 Upgrade Required — указывает на то, что сервер отказывается выполнять запрос с использованием текущего протокола, но может захотеть сделать это после того, как клиент обновится до другого протокола (используется, когда версия приложения уже не поддерживается и пользователю предлагается обновить приложение при получении данной ошибки).

429 Too Many Requests — запрос отклоняется из-за ограничения скорости (слишком много запросов за определённый промежуток времени).

5xx

Операция завершилась с ошибкой по вине сервера (или не смог сразу определить, что по вине клиента)

Конкретные 5xx ошибки не фиксируем обычно в спецификации API, но если необходима необычная обработка, то фиксируйте (к примеру, нужна определённая заглушка на ошибку временной неработоспособности сервера — 503 Service Unavailable).

Структура и проектирование моделей

В файл models.yaml я обычно выношу components, который в свою очередь включает:

  • schemas — модели,

  • parameters — параметры.

Пример структуры файла:

components:
   schemas:
     UpdatedOrderResponse:
       type: object
       description: Модель для обновлённых полей заказа после выполнения действия над ним.
       properties:
         status:
           $ref: "#/components/schemas/ExtendedOrderStatus"
         actions:
           type: array
           description: |
             Список действий, доступных над заказом.
             Список пуст, если нет доступных действий.
           items:
              $ref: "#/components/schemas/OrderAction"
       required:
         - status
         - actions
     ReceiverType: # модель, которая содержит ограничения возможных значений по типу плательщика
       type: string
       enum: [individual, entity]
       description: |
         Тип плательщика:
         - individual — физическое лицо
         - entity — юридическое лицо
 
   parameters: # параметры, которые можно переиспользовать в параметрах запроса
     filter_type: 
       name: filter_type 
       in: query
       description: | 
         Тип фильтра заказов пользователя: 
         - all — все заказы
         - current — текущие
         - done — выполненные
       schema:
         enum: ["all", "current", "done"] 
          type: string

Комментарий (description) и пример (example) — очень важная часть спецификации. Применимо как к методам, так и к моделям.
Уделяйте большое внимание этим полям и описывайте максимально понятно, вкладывайте контекст, логику, примеры — пишем как можно больше (в пределах разумного, конечно, описывать суперподробно user.name не стоит).

Типы параметров

С помощью ключевого слова type задаётся тип данных. Типы могут быть следующими (основные):

  • string — строка текста.

  • number — включает в себя и целые числа, и числа с плавающей точкой.

  • integer — только целые числа.

  • boolean — в логическом типе boolean представлено два возможных значения: true и false.

  • array — массив.

  • object — объекты — коллекция пар «ключ и значение».

Строка

  • Длину строки можно ограничить, используя для этого minLength и maxLength.

  • Ключевое слово pattern позволяет определить шаблон регулярного выражения для строки — значения, которые могут быть использованы в строке. Для задания pattern используется синтаксис регулярного выражения из JavaScript (pattern: '^\d{3}-\d{2}-\d{4}')." — конца строки. Без ^… $ шаблон соответствует любой строке, содержащей указанное регулярное выражение.

  • Ключевое слово format используется для того, чтобы задать формат строки, например, один из них: date (2017-07-21), date-time (2017-07-21T17:32:28Z), password, byte, binary

К примеру, для передачи файла используется:

avatar:          # изображение, встроенное в JSON
 description: Base64-encoded contents of the avatar 
 type: string
  format: byte

Числа

  • Для указания диапазона возможных значений можно использовать ключевые слова minimum и maximum (minimum ≤value≤ maximum).

  • Чтобы исключить граничные значения, укажите exclusiveMinimum: true и exclusiveMaximum: true.

count:      	
  description: Суммарное количество товаров в заказе 
  type: integer
  example: 6
  maximum: 25
id:
  description: Идентификатор пользователя
  type: integer
  format: int64

Массивы

  • С помощью minItems и maxItems можно задавать минимальную и максимальную длины массива. Если не использовать minItems, то массив может быть пустым.

  • Элементы массива описываем отдельной моделью, если они представляют собой коллекцию.

# Элементы массива отдельной моделью
actions:         
  description: |
    Список действий, доступных над заказом. 
    Список пуст, если нет доступных действий. 
  type: array
  items:
    $ref: "#/components/schemas/OrderAction"
# Массив строк
categories:
  description: |
    id категорий товаров первого уровня, в которые входят товары данной акции 
  type: array
  items:
    type: string

Объекты

  • По умолчанию все элементы коллекции необязательные. Можно указать список обязательных элементов с помощью слова required.

  • Коллекция также может быть вложенной и включать в себя коллекцию. В таком случае коллекцию оформляем отдельным объектом и даём на него ссылку для удобства всех членов команды.

Проверка валидности спецификации OpenAPI

Минимальная проверка спецификации API может быть проведена путём визуализации спецификации OpenAPI. Если вы делили спецификацию на разные файлы для методов и моделей, то визуализацию надо запускать, находясь в файле с методами.

Надо проверить, что спецификация визуализируется, все параметры отрендерены, прописаны обязательные и nullable-поля. Запросы и ответы также отрендерены, и не отображаются ошибки.

Если спецификация не рендерится, на что обратить внимание:

  • интервалы. Проверьте, что все объекты находятся на своём уровне;

  • ссылки. Проверьте, что все ссылки корректны и они ссылаются на существующие модели.

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

Дополнительная информация для ознакомления

Headers

Основные заголовки:

  • Accept-Charset — способ клиента сказать, в какой кодировке нужны данные (UTF-8, ASCII, whatever). Обычно всегда используется UTF-8, и менять не нужно.

  • Accept-Encoding (аналог с сервера — Content-Encoding) — то, как данные от сервера закодированы, обычно речь про алгоритм сжатия. Например, gzip.

  • Accept-Language (аналог с сервера — Content-Language) — то, какой язык хочет получить клиент. Использовать можно для мультиязычных сервисов.

  • Accept (аналог с сервера — Content-Type) — форматы данных, которые клиент поддерживает, эти форматы называются MIME-типами. Например, application/json. Такое часто бывает при передаче файлов или когда хотим открыть файл в вебе, здесь нужно правильно установить MIME-тип.

  • Cookie — это способ хранить состояние. Как это работает:

    • сначала сервер просит клиента установить cookies (Set-Cookie);

    • клиент при обращении отправляет их серверу в заголовке с ключом Cookie.

Если вы используете Cookie для передачи токена, то в таком случае обязательны параметры:

  • secure=true

  • httponly=true

  • samesite=strict

Общие правила при проектировании спецификации OpenAPI, которые были выработаны совместно с командой

Правило

Описание

Использовать kebab-case для URL

Мы используем: /set-unread

В других командах могут делать и так: /setUnread или /set_unread

Использовать camelCase для параметров пути

Мы используем: /orders/{orderId}

В других командах могут делать и так: /orders/{order_id} или /orders/{OrderId}

Использовать множественное число для коллекций

Мы используем: GET /users

В других командах могут делать и так: GET /user или GET /User

Не использовать глаголы в URL ресурсов

Вместо этого используем HTTP-методы для описания операций.

Мы используем: POST /courses/{courseId} или GET /courses

В других командах могут делать и так: POST /updatecourse/{courseId} или GET /getcourses

Использовать snake_case для JSON-свойств

Мы используем:

{

  "user_name": "Ванька Петров",

  "user_id": "1"

}

В других командах могут делать и так:

{

  "userName": "Ванька Петров",

  "userId": "1"

}

Не используем kebab-case:

{

  "user-name": "Ванька Петров",

  "user-id": "1"

}

Использовать глаголы в URL операций

Для функций, которые выполняют определённые действия на сервере и при этом не являются CRUD-операцией:

Мы используем: POST /messages/{messageId}/resend

Использовать простой порядковый номер для версий

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

http://api.domain.com/v1/shops/3/products

Указывать количество элементов в ответе на запрос

Если есть возможность возвращать общее количество элементов и это не скажется плохо на вашей производительности — возвращайте.

{

 "users": [

     ...

 ],

  "offset": 0,

 "total": 34

}

Не передавать аутентификационные токены в URL

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

Мы используем: заголовки авторизации и Cookies.

Исключения: GET /resourse?token=authenticaiton_token 

Использовать HTTP-методы для CRUD-операций

В этом и есть их смысл.

Опять же — если вы следуете принципам REST.

Заключение

В этой статье мы познакомились со структурой спецификации OpenAPI и примерами её использования.

Ещё раз напомню, что REST — это архитектурный стиль, а не стандарт. Поэтому всё сказанное в этой статье основано на реальном опыте системных аналитиков одной из ИТ-компаний нашей необъятной страны и не является призывом к обязательному применению.

В разных компаниях при использовании разных языков программирования или просто, потому что так «исторически сложилось», могут прибегать к тем или иным решениям, о которых мы в этой статье не поговорили или они не соответствуют описанному. Если статья для вас не имеет никакого смысла, буду рада услышать ваши комментарии о том, какие практики проектирования API используете вы.

В этой статье будет только одна РЕКОМЕНДАЦИЯ: придерживайтесь того, что принято в вашей компании и команде, а данная статья может стать неплохой основой для формирования корпоративного гайдлайна, если у вас такового нет ?

Всем добра!

Комментарии (5)


  1. pvzh
    28.06.2024 20:35
    +4

    Design API First-подход в первую очередь помогает создавать API более эффективно

    Не соглашусь. Составление руками в Swagger Editor это издевательство. Зачем аналитикам вообще заходить на территорию даже не техлида, а мидла. REST и OpenAPI это базовая база, и регулярки тоже. Разве мидл-кодеру не достаточно списка действий и параметров и краткого описания их назначения с возможными значениями в свободной форме? Кодер готовит черновик, через автогенерацию собирается схема, аналитик через Swagger UI проверяет и при необходимости указывает что подправить.


    1. TerekhinSergey
      28.06.2024 20:35
      +2

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

      Мне все же видится, что schema-First подход более интересен, потому что позволяет развязать команды фронта, бека и тестирования и согласовать API максимально дёшево - всё же yaml поправить гораздо быстрее и дешевле, чем код бекенда. Также такой подход неплохо срабатывает в случае, когда API является внешним - проще создать схему (при её отсутствии) и нагенерить себе по ней клиентов, тестовых заглушек и чего угодно ещё

      Но, как обычно, есть нюансы) Например, существующие инструменты генерации северного когда под .net не умеют в minimal API, odata и прочие штуки - генератор придётся писать самим. То же самое с клиентом - если используется библиотека, которая не поддерживается генератором, то попадаешь на разработку собственного инструмента


    1. tik4
      28.06.2024 20:35
      +2

      Поддержу, у нас аналитики просто пишут внятные описания api в свободной форме (обычно таблички в конфе) со списком входных/выходных параметров, действий и ошибок. И потом вместе с тестировщиками проверяют через swagger ui, что все сделано корректно.
      Не, ну можно и на аналитиков повесить делать api через editor, но мне кажется, что более разумно посвятить это время на улучшение качества аналитики или проработку большего количества фич)


      1. Crash13
        28.06.2024 20:35

        Честно говоря, одно другому не мешает.


    1. yulskaya_victoriya Автор
      28.06.2024 20:35

      Отчасти согласна с теми, кто выступил против того, чтобы аналитики писали спецификацию openAPI. Не всем и не всегда это нужно / полезно / оправдано по ресурсам.

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

      По поводу того, что это издевательство - спорный момент. Тебе что хорошую документацию в конфлюенс написать, что спецификацию openAPI, по времени не сильно большая разница. Потому что тебе также нужно приложить примеры запросов / ответов, расписать каждый параметр. В Swagger Editor писать может и да, издевательство) но с применением любой IDE с нужными расширениями это уже не такое и издевательство - с генерацией шаблона, автодополнениями, быстрой навигацией по ссылкам на модели и тд.

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