API

OpenAPI: контракт, специфікація та документація API

Що таке OpenAPI, як влаштована специфікація YAML/JSON, чим OpenAPI відрізняється від Swagger UI, коли обирати contract-first або code-first, та як уникати типових помилок.

OpenAPI: контракт, специфікація та документація API

У попередніх матеріалах ми проєктували URL, формати даних, статус-коди, помилки, пагінацію та безпеку. Але всі ці рішення мають одну спільну проблему: якщо вони існують лише «в голові команди» або розкидані по коду, клієнтський розробник не бачить цілісного контракту. Саме цю проблему і вирішує OpenAPI.

1. Навіщо OpenAPI, якщо код уже існує

Уявімо реальну ситуацію. Команда бекенду каже: «Ендпоінт готовий, просто виклич POST /v1/orders». Команда фронтенду питає:

  • Які поля обов'язкові?
  • Який формат має created_at?
  • Які статус-коди повертаються при помилках?
  • Чи потрібно передавати Authorization?
  • Який вигляд має 422, а який 409?

Якщо відповіді на ці питання живуть лише в коді сервера, то клієнтський розробник змушений або читати чужий бекенд, або експериментувати через Postman, або ставити десятки уточнювальних питань. Це дорого, повільно і небезпечно, бо API перетворюється з контракту на набір припущень.

OpenAPI розв'язує цю проблему: він формалізує HTTP API в машинозчитуваний документ. Такий документ може одночасно читати людина, IDE, генератор клієнтів, тестові інструменти та система перевірки контракту.

Що ви отримаєте

  • Чітке розуміння, що саме описує OpenAPI
  • Навичку читати структуру paths, responses, components, schemas
  • Вміння відрізняти специфікацію від інструментів на кшталт Swagger UI
  • Практичний YAML-приклад невеликого API

Пререквізити


2. Що таке OpenAPI насправді

OpenAPI Specification (OAS), або просто OpenAPI, це стандарт опису HTTP API у форматі YAML або JSON.

Ключова думка: OpenAPI описує контракт взаємодії, а не реалізацію сервера. У специфікації немає SQL-запитів, внутрішніх сервісів, middleware чи класів доменної моделі. Там є лише те, що бачить зовнішній споживач API:

  • які ресурси існують;
  • які HTTP-методи підтримуються;
  • які параметри треба передати;
  • яке тіло запиту очікується;
  • які відповіді можливі;
  • які схеми даних використовуються;
  • яка авторизація потрібна.
Думайте про OpenAPI як про юридичний текст договору, а не як про код сервера. Код можна переписати. Контракт, уже виданий клієнтам, не можна змінювати без наслідків.

OpenAPI, Swagger, Swagger UI: де тут плутанина

Тут часто виникає термінологічний хаос, тому розмежуємо поняття одразу.

ТермінЩо це такеРоль
OpenAPIСтандарт специфікаціїОписує контракт API
Swagger SpecificationІсторична назва попередніх версій стандартуСьогодні майже завжди мають на увазі OpenAPI
Swagger UIВебінтерфейс для візуалізації специфікаціїПоказує документацію та дає робити тестові запити
Swagger EditorРедактор специфікаційДає писати і перевіряти YAML/JSON
Code GeneratorІнструмент генерації клієнта або серверного каркасаВикористовує специфікацію як вхідні дані

Проблема в тому, що в побутовій мові розробники часто кажуть «Swagger», маючи на увазі будь-що з цього списку. Але технічно коректніше розділяти:

  • OpenAPI — це формат договору;
  • Swagger UI — це спосіб показати договір;
  • generator — це спосіб використати договір.

3. Що саме дає OpenAPI команді

OpenAPI цінний не тому, що «гарно виглядає в браузері». Його сила в тому, що один документ стає єдиним джерелом правди для кількох ролей одночасно.

Loading diagram...
flowchart LR
    A["Backend Team"] --> B["OpenAPI Contract"]
    C["Frontend Team"] --> B
    D["QA / Testing"] --> B
    E["Documentation UI"] --> B
    F["SDK / Client Generators"] --> B
    G["Contract Validation"] --> B

Практична користь:

СценарійЯк допомагає OpenAPI
ДокументаціяSwagger UI або інші сайти будують reference автоматично
Розробка клієнтаГенеруються DTO, методи виклику, типи помилок
ТестуванняМожна перевіряти, чи сервер не порушив контракт
Рев'ю API-дизайнуКонтракт видно до реалізації коду
OnboardingНовий інженер читає специфікацію, а не 30 контролерів
OpenAPI не робить API «якісним» автоматично. Якщо погано спроєктувати статус-коди, назви полів або помилки, то специфікація лише дуже точно зафіксує цей поганий дизайн.

4. Анатомія OpenAPI-документа

OpenAPI-документ зазвичай має кілька великих частин. Кожна відповідає на окремий клас запитань.

Крок 1: Метадані документа

Секції openapi, info і servers відповідають на питання:

  • яку версію стандарту ми використовуємо;
  • як називається API;
  • де воно доступне;
  • який базовий URL вважати коренем.

Крок 2: Операції та маршрути

Секція paths описує URL та HTTP-методи. Саме тут живуть get, post, put, delete, параметри маршруту, query-параметри, requestBody і responses.

Крок 3: Повторно використовувані схеми

Секція components потрібна, щоб не дублювати структури всюди вручну. Тут зберігають:

  • schemas для типів даних;
  • parameters для спільних параметрів;
  • responses для типових відповідей;
  • securitySchemes для опису авторизації.

Крок 4: Правила безпеки

Секції securitySchemes і security формалізують, які механізми аутентифікації потрібні: Bearer token, API key, OAuth2 тощо.

Мінімальна мапа структури

Каркас специфікації
openapi: 3.1.0
info:
  title: Coffee API
  version: 1.0.0
servers:
  - url: https://api.example.com
paths:
  /v1/orders:
    post:
      summary: Create order
components:
  schemas: {}
  securitySchemes: {}

Цей каркас нічого корисного ще не описує, але показує головну ідею: документ читається зверху вниз, від загальних метаданих до конкретних операцій і повторно використовуваних типів.

Каталог важливих елементів і їх варіацій

До цього моменту ми бачили лише великий каркас. Але реальна робота з OpenAPI майже завжди відбувається на рівні маленьких фрагментів: сьогодні вам треба згадати, як описати multipart/form-data, завтра — як оформити oneOf, післязавтра — як документувати query-параметр-масив.

Нижче наведено саме такий довідковий шар: короткі приклади, які можна читати незалежно один від одного.

Думайте про цю частину статті як про робочий конструктор. Ви не зобов'язані використовувати всі елементи одразу. OpenAPI добрий тоді, коли контракт точний, а не тоді, коли в нього механічно вставили кожну можливу секцію.

info: не лише title і version

Секція info може містити не тільки назву та версію, а й метадані для людей, юристів і партнерів.

info — розширений приклад
info:
  title: Billing API
  version: 2.3.0
  summary: API для рахунків, платежів і повернень
  description: >
    Публічний контракт платіжної платформи для партнерів.
  termsOfService: https://example.com/terms
  contact:
    name: API Support
    email: api-support@example.com
    url: https://example.com/support
  license:
    name: Commercial License
    url: https://example.com/license

Практичний сенс:

  • summary дає короткий контекст;
  • contact зменшує кількість «кому писати, якщо щось не працює?»;
  • license та termsOfService важливі для публічних або партнерських API.

servers: фіксовані URL та змінні

Найпростіший варіант ми вже бачили: список готових URL. Але servers підтримує і змінні.

servers:
  - url: https://api.example.com
    description: Production
  - url: https://sandbox.example.com
    description: Sandbox

Змінні корисні, коли адреса API структурно стабільна, але середовище або базовий префікс можуть змінюватися.

tags: не декоративна дрібниця

Багато хто сприймає tags як косметику для UI. Насправді це основний інструмент навігації у великих документах.

tags
tags:
  - name: Orders
    description: Операції із замовленнями
  - name: Payments
    description: Платежі, повернення та статуси транзакцій
  - name: Admin
    description: Адміністративні endpoint-и

Якщо у вас 80 операцій і немає тегів, документація швидко стає непридатною до використання.

parameters: чотири місця розташування

Параметри в OpenAPI описуються не абстрактно, а з чітким in.

parameters:
  - name: orderId
    in: path
    required: true
    schema:
      type: string
      format: uuid

Для path параметрів required: true є фактично обов'язковим, бо інакше сам шлях втрачає сенс.

style і explode: як серіалізуються параметри

Один і той самий масив можна передати кількома способами. Саме для цього існують style і explode.

inТиповий styleЩо це означає
pathsimple, label, matrixяк параметр вбудовується в URL-шлях
queryform, spaceDelimited, pipeDelimited, deepObjectяк параметр потрапляє в query string
headersimpleяк значення кодується в заголовку
cookieformяк значення кодується в cookie
query-масив
parameters:
  - name: tags
    in: query
    style: form
    explode: true
    schema:
      type: array
      items:
        type: string

Такий опис зазвичай означає формат на кшталт:

?tags=coffee&tags=arabica&tags=discount

Ще один приклад:

deepObject для query-об'єкта
parameters:
  - name: filter
    in: query
    style: deepObject
    explode: true
    schema:
      type: object
      properties:
        status:
          type: string
        minPrice:
          type: number

Це вже відповідає формату:

?filter[status]=active&filter[minPrice]=100
Не описуйте style і explode, якщо ваш бекенд насправді не підтримує таку серіалізацію. Це один із найпідступніших способів зробити документацію формально валідною, але практично хибною.

requestBody: не лише JSON

requestBody потрібен не всім методам, але коли він є, OpenAPI дозволяє дуже точно вказати тип контенту.

requestBody:
  required: true
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/CreateOrderRequest'

Більшість статей обмежуються прикладом 200 і 404. Але OpenAPI підтримує значно більше.

content: одна відповідь, кілька media type

OpenAPI дозволяє описати кілька форматів представлення тієї самої відповіді.

JSON + XML
responses:
  '200':
    description: Товар знайдено
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Product'
      application/xml:
        schema:
          $ref: '#/components/schemas/Product'

У сучасних HTTP API найчастіше достатньо application/json, але стандарт не обмежується лише ним.

components: не лише schemas

У багатьох командах components асоціюється лише зі schemas. Це спрощення. Насправді components може бути центральним місцем для всіх повторно використовуваних шматків контракту.

components — основні секції
components:
  schemas: {}
  responses: {}
  parameters: {}
  examples: {}
  requestBodies: {}
  headers: {}
  securitySchemes: {}
  links: {}
  callbacks: {}

Найчастіше реально використовують:

  • schemas;
  • responses;
  • parameters;
  • securitySchemes.

Але для великих API також корисні requestBodies, headers, examples і links.

reusable requestBody
components:
  requestBodies:
    CreateOrderBody:
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/CreateOrderRequest'

paths:
  /v1/orders:
    post:
      requestBody:
        $ref: '#/components/requestBodies/CreateOrderBody'
reusable header
components:
  headers:
    RequestId:
      description: Унікальний ідентифікатор запиту
      schema:
        type: string

responses:
  '200':
    description: OK
    headers:
      X-Request-Id:
        $ref: '#/components/headers/RequestId'
reusable example
components:
  examples:
    ReadyOrder:
      summary: Готове замовлення
      value:
        id: 42
        status: ready

Практичний принцип тут простий: якщо один і той самий фрагмент починає копіюватися в кілька місць, подумайте, чи не час винести його в components.

example vs examples

Це маленька, але дуже часта точка плутанини.

schema:
  type: string
  example: pending

example добре підходить для простих полів. examples краще, коли треба показати кілька реальних сценаріїв.

schemas: не лише примітиви

У components/schemas можна описувати як прості типи, так і складні композиції.

OrderStatus:
  type: string
  enum: [pending, paid, canceled]

Практична інтерпретація:

  • allOf добре підходить для наслідування або розширення базової помилки;
  • oneOf означає «рівно одна форма з кількох»;
  • anyOf означає «може відповідати кільком формам одразу»;
  • readOnly і writeOnly допомагають не плутати поля, що приходять лише від клієнта або лише від сервера.

discriminator: коли oneOf стає складним

Якщо oneOf описує поліморфні об'єкти, часто потрібен discriminator, щоб клієнт зрозумів, яку саме форму він отримав.

discriminator
PaymentMethod:
  oneOf:
    - $ref: '#/components/schemas/CardPayment'
    - $ref: '#/components/schemas/BankTransferPayment'
  discriminator:
    propertyName: type
    mapping:
      card: '#/components/schemas/CardPayment'
      bank_transfer: '#/components/schemas/BankTransferPayment'

securitySchemes: основні варіанти

securitySchemes:
  bearerAuth:
    type: http
    scheme: bearer
    bearerFormat: JWT

Після визначення схеми її ще треба застосувати:

security на рівні операції
security:
  - bearerAuth: []

Або глобально для всього документа:

security на рівні документа
security:
  - bearerAuth: []

callbacks і webhooks: просунуті сценарії

Більшість OpenAPI-документів ніколи не використовують callbacks, але для асинхронних інтеграцій це дуже цінний інструмент.

Сенс тут такий: клієнт передає URL зворотного виклику, а сервер документує, який HTTP-запит прийде туди пізніше.

externalDocs: коли специфікації замало

OpenAPI добре описує контракт, але не замінює повністю гіди, туторіали та бізнесові пояснення.

externalDocs
externalDocs:
  description: Повний гайд для інтеграції
  url: https://developer.example.com/guides/orders

Цей елемент корисний, коли вам треба пов'язати формальний контракт із навчальним або бізнесовим контекстом.


5. Повний приклад невеликої специфікації

Нижче наведено компактний, але реалістичний OpenAPI-документ для створення і читання замовлень.

openapi.yaml
openapi: 3.1.0
info:
  title: Coffee Orders API
  version: 1.0.0
  summary: API для створення та перегляду замовлень кави
  description: >
    Контракт HTTP API для мобільного додатка кав'ярні.
    API дозволяє створювати замовлення та отримувати їх за ідентифікатором.

servers:
  - url: https://api.example.com
    description: Production
  - url: https://sandbox.api.example.com
    description: Sandbox

tags:
  - name: Orders
    description: Операції із замовленнями

paths:
  /v1/orders:
    post:
      tags: [Orders]
      operationId: createOrder
      summary: Створити нове замовлення
      description: >
        Приймає дані нового замовлення, виконує валідацію
        і повертає створений ресурс.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
            examples:
              lungo:
                summary: Замовлення лунго
                value:
                  recipeId: lungo
                  volumeMl: 300
                  sugar: 1
      responses:
        '201':
          description: Замовлення створено
          headers:
            Location:
              description: URL створеного ресурсу
              schema:
                type: string
                format: uri
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'

  /v1/orders/{orderId}:
    get:
      tags: [Orders]
      operationId: getOrderById
      summary: Отримати замовлення за ідентифікатором
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrderId'
      responses:
        '200':
          description: Замовлення знайдено
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  parameters:
    OrderId:
      name: orderId
      in: path
      required: true
      description: UUID замовлення
      schema:
        type: string
        format: uuid

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  responses:
    BadRequest:
      description: Некоректний формат запиту
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Unauthorized:
      description: Користувач не аутентифікований
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    NotFound:
      description: Замовлення не знайдено
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    ValidationError:
      description: Помилка валідації полів
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError'

  schemas:
    CreateOrderRequest:
      type: object
      additionalProperties: false
      required:
        - recipeId
        - volumeMl
      properties:
        recipeId:
          type: string
          description: Код рецепта
          example: lungo
        volumeMl:
          type: integer
          minimum: 50
          maximum: 500
          description: Об'єм напою в мілілітрах
          example: 300
        sugar:
          type: integer
          minimum: 0
          maximum: 5
          description: Кількість ложок цукру
          example: 1

    Order:
      type: object
      required:
        - id
        - recipeId
        - volumeMl
        - status
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        recipeId:
          type: string
        volumeMl:
          type: integer
        sugar:
          type: integer
          nullable: true
        status:
          type: string
          enum: [pending, brewing, ready, canceled]
        createdAt:
          type: string
          format: date-time

    ApiError:
      type: object
      required:
        - status
        - reason
        - developerMessage
      properties:
        status:
          type: integer
          example: 401
        reason:
          type: string
          example: authentication_required
        developerMessage:
          type: string
          example: Bearer token is missing or invalid

    ValidationError:
      allOf:
        - $ref: '#/components/schemas/ApiError'
        - type: object
          properties:
            errors:
              type: array
              items:
                type: object
                required: [field, message]
                properties:
                  field:
                    type: string
                  message:
                    type: string

Як читати цей документ

info і servers

Блок info описує сам документ: назву, версію, короткий опис. Блок servers показує, проти яких середовищ клієнт може працювати. Це не просто декоративна інформація: інструменти можуть підставляти Production або Sandbox як базову адресу для тестових запитів.

paths

Тут видно дві операції:

  • POST /v1/orders створює ресурс;
  • GET /v1/orders/{orderId} читає ресурс.

Кожна операція має власні:

  • summary і description для документації;
  • operationId для генераторів SDK;
  • security для вимог авторизації;
  • parameters, requestBody, responses для контракту взаємодії.

requestBody

У POST /v1/orders тіло запиту оголошене явно. Специфікація не просто каже «передайте JSON», а визначає:

  • MIME-тип через application/json;
  • конкретну схему через $ref;
  • приклади через examples.

Саме тому якісний OpenAPI-документ корисніший за звичайний Markdown-опис. Він не обмежується словами, а формалізує структуру.

responses

У кожного статус-коду є свій опис. Це критично важливо: без цього клієнт бачить лише «може бути 400», але не знає, яке тіло повернеться і як його парсити.

Зверніть увагу на два прийоми:

  • 201 описано локально, бо там унікальна відповідь з Location;
  • 400, 401, 404, 422 винесені в components/responses, щоб не дублювати один і той самий фрагмент у десяти ендпоінтах.

components/schemas

Секція schemas описує форми даних. Це фактично словник типів, який використовує весь документ.

Наприклад:

  • CreateOrderRequest описує payload вхідного запиту;
  • Order описує успішну відповідь;
  • ApiError і ValidationError описують формат помилок.

Якщо ви змінюєте схему в одному місці, усі $ref, що посилаються на неї, автоматично використовують оновлений контракт.


6. Ключові елементи, які часто недооцінюють

operationId

Для людини operationId може виглядати необов'язковим. Але для генератора клієнта це майже ім'я методу.

Без operationId ви ризикуєте отримати в SDK щось на кшталт postV1Orders або getV1OrdersOrderId, що складно читати й підтримувати. Імена на кшталт createOrder, getOrderById, cancelOrder роблять сгенерований код набагато якіснішим.

examples

Схема показує форму даних, але приклад показує семантику. Поле volumeMl: integer не пояснює, чи типове значення це 50, 300 чи 5000. Приклад показує нормальний, очікуваний сценарій використання.

additionalProperties: false

Цей прапорець означає: «не приймайте довільні зайві поля». Він корисний, коли ви хочете жорсткіший контракт і не бажаєте мовчазно проковтувати сміттєві дані.

Використовуйте жорсткі обмеження усвідомлено. Надто сувора схема може перетворити дрібне еволюційне розширення API на breaking change.

securitySchemes та security

Безпека в OpenAPI не повинна лишатися «текстом у README». Якщо API вимагає Bearer token, це має бути описано структуровано. Інакше документація виглядатиме повною, але фактично не дозволить коректно використати API.


7. Contract-first vs Code-first

Це одна з головних стратегічних розвилок у роботі з OpenAPI.

Спочатку команда проєктує OpenAPI-специфікацію, рев'ює її і лише потім починає реалізацію сервера та клієнтів.

Цей підхід сильний тоді, коли:

  • API споживає кілька команд;
  • контракт має бути стабільним і погодженим заздалегідь;
  • важливо провести дизайн-рев'ю до написання коду;
  • потрібна генерація SDK або мок-сервера з контракту.

Який підхід кращий

Універсально «кращого» підходу не існує. Потрібно дивитися на організаційний контекст.

СценарійКращий стартовий вибір
Публічне API для зовнішніх інтеграційcontract-first
B2B API з тривалим життєвим цикломcontract-first
Внутрішній сервіс невеликої командиcode-first
Прототип або MVPcode-first, але з подальшим контрактним прибиранням
Якщо ваш API вже в продакшні, практичне правило таке: навіть у code-first режимі команда повинна почати ставитися до згенерованого OpenAPI як до публічного артефакту, який перевіряють у code review так само уважно, як і код.

8. Типові помилки в OpenAPI-специфікаціях

Погана специфікація не краща за відсутність специфікації. Вона створює фальшиве відчуття надійності.


9. Як OpenAPI пов'язаний з іншими темами API-дизайну

OpenAPI не живе окремо від решти архітектурних рішень. Він лише фіксує їх у формальному вигляді.

Статус-коди

Те, що ми вивчали в HTTP-методах і статус-кодах, у OpenAPI з'являється як responses.

Валідація

Те, що ми проєктували у валідації та помилках, у OpenAPI відображається через requestBody, schemas і моделі ApiError.

Безпека

Те, що ми розглядали в безпеці API, у OpenAPI формалізується як securitySchemes і security.

Процес проєктування

У матеріалі про процес проєктування API OpenAPI є фінальним артефактом, який фіксує вже прийняті рішення.

10. Інструменти та робочий процес

На практиці команда рідко працює зі специфікацією «вручну і в вакуумі». Зазвичай навколо OpenAPI існує цілий pipeline.

  1. Команда проєктує або генерує специфікацію.
  2. Специфікація перевіряється валідатором і проходить review.
  3. Документаційний UI показує reference.
  4. Генератор клієнтів або контрактні тести використовують документ як джерело правди.
  5. Зміни у специфікації порівнюються на предмет breaking changes.
У .NET-екосистемі OpenAPI часто генерується з ASP.NET Core через вбудовані механізми або бібліотеки на кшталт Swagger tooling. Але це лише один зі способів отримати документ, а не сутність теми OpenAPI.

11. Практичні завдання

Рівень 1: Базовий

Рівень 2: Аналітика контракту

Рівень 3: Мініпроєкт


12. Підсумок

OpenAPI потрібен не для «гарної Swagger-сторінки», а для формалізації контракту між сервером і споживачами API. Якщо дизайн API є хорошим, OpenAPI робить його видимим, перевірюваним і придатним для автоматизації. Якщо дизайн API є поганим, OpenAPI дуже швидко це викриває.

Практичний висновок такий:

  • спочатку спроєктуйте поведінку API;
  • потім зафіксуйте її у специфікації;
  • ставтеся до OpenAPI як до публічного контракту, а не як до другорядного артефакту tooling.

Саме в такому вигляді специфікація стає частиною інженерної дисципліни, а не просто ще одним YAML-файлом у репозиторії.