← назад к разделу

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

Что такое OpenAPI и зачем он нужен

Раньше API документировали в Word-файлах, Confluence-страницах или вовсе «на словах». Проблема понятна: документация живёт отдельно от кода, устаревает и врёт.

OpenAPI — это стандарт машиночитаемого описания REST API в формате YAML или JSON. Один файл описывает все эндпоинты: URL, методы, параметры, тела запросов и ответов, коды ошибок.

Что это даёт на практике:

  • Swagger UI / Redoc — интерактивная документация генерируется автоматически.
  • Client SDK — openapi-generator создаёт типизированный клиент на Java, TypeScript, Python и других языках прямо из спецификации.
  • Postman — коллекции запросов импортируются из файла, не пишутся руками.
  • Contract testing — тесты проверяют, что реализация соответствует спецификации.

Хорошо оформленный OpenAPI — это не просто документация, это инфраструктура вокруг вашего API.

operationId: имя операции

Каждый эндпоинт в OpenAPI получает поле operationId. Это уникальное имя операции — именно его используют генераторы SDK как имя метода.

Без operationId генератор придумывает имя сам. Результат обычно выглядит как postOrdersOrderIdConfirm — неудобно читать и ещё хуже использовать.

Правило: уникальный, camelCase, формат действие + ресурс.

/api/v1/orders:
  get:
    operationId: getOrders       # список
  post:
    operationId: createOrder     # создание

/api/v1/orders/{orderId}:
  get:
    operationId: getOrder        # один элемент
  put:
    operationId: updateOrder     # полная замена
  patch:
    operationId: patchOrder      # частичное обновление
  delete:
    operationId: deleteOrder     # удаление

/api/v1/orders/{orderId}/confirm:
  post:
    operationId: confirmOrder    # действие над ресурсом

Так клиентский код выглядит читаемо: orderService.confirmOrder(orderId), а не загадочный автоматически сгенерированный вариант.

Соглашение по именам:

  • get{Resource} — один объект (getOrder)
  • get{Resources} — список (getOrders)
  • create{Resource} — создание через POST
  • update{Resource} — полная замена через PUT
  • patch{Resource} — частичное обновление через PATCH
  • delete{Resource} — удаление
  • {глагол}{Resource} — бизнес-действие (confirmOrder, cancelOrder)
  • search{Resources} — поиск через POST с телом

tags: группировка эндпоинтов

Без группировки Swagger UI показывает плоский список из ста эндпоинтов — найти что-то невозможно. Поле tags решает это: эндпоинты группируются в разделы по ресурсу.

Правило: один тег на ресурс, имя во множественном числе с заглавной буквы.

tags:
  - name: Orders
    description: 'Управление заказами'
  - name: Users
    description: 'Управление пользователями'
  - name: Payments
    description: 'Платежи'

/api/v1/orders:
  get:
    tags: [Orders]
  post:
    tags: [Orders]

/api/v1/orders/{orderId}/confirm:
  post:
    tags: [Orders]    # действие относится к тегу родительского ресурса

Частая ошибка — создавать отдельный тег для действий: OrderActions, OrderOperations. Этого не нужно. Эндпоинт POST /orders/{orderId}/confirm относится к тегу Orders, не к придуманному Confirmations.

Параметры пути: уникальные имена в спецификации

Здесь есть тонкость, которая удивляет новичков. В коде и проектировании URL обычно пишут {id} — контекст и так понятен из пути. Но в OpenAPI это нельзя:

# Так не работает в Swagger UI — одинаковые имена в одном пути
/api/v1/orders/{id}/items/{id}:
  get:
    parameters:
      - name: id   # какой id? первый или второй?
        in: path

Swagger UI и Redoc не умеют работать с одинаковыми именами параметров в одном пути. Поэтому в OpenAPI-спецификации параметры именуют уникально:

/api/v1/orders/{orderId}/items/{itemId}:
  get:
    parameters:
      - name: orderId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: itemId
        in: path
        required: true
        schema:
          type: string
          format: uuid

Это требование инструментов, а не семантическое правило. В документации к самому API и в коде можно продолжать писать {id} — контекст устраняет неоднозначность.

summary и description

summary — это короткое описание эндпоинта, которое видно в Swagger UI рядом с URL. Правило простое: до 80 символов, фраза, не предложение.

description — развёрнутое объяснение в формате Markdown. Добавляйте его только когда логика эндпоинта неочевидна: особые условия, переходы состояний, ограничения.

/api/v1/orders/{orderId}/confirm:
  post:
    operationId: confirmOrder
    tags: [Orders]
    summary: 'Подтвердить заказ'
    description: |
      Переводит заказ из статуса CREATED в CONFIRMED.
      Заказ должен содержать хотя бы одну позицию.
      После подтверждения изменение состава заказа невозможно.

Пустой description лучше, чем description: 'Confirm order' — бессмысленное дублирование summary.

Типичные ошибки в REST-дизайне

Это разбор частых проблем, с которыми сталкиваются при проектировании и ревью REST API.

Глагол в URL

# Неправильно
GET  /api/v1/getOrders
POST /api/v1/cancelOrder
POST /api/v1/orders/doConfirm

# Правильно
GET  /api/v1/orders
POST /api/v1/orders/{id}/cancel
POST /api/v1/orders/{id}/confirm

В REST метод HTTP уже несёт смысл действия. Глагол в URL — это дублирование и нарушение соглашений. Единственное место, где глагол в URL уместен — бизнес-действия (/cancel, /confirm, /approve), и там он стоит в конце пути после ID ресурса.

CamelCase и snake_case в пути

Пути URL пишутся в kebab-case (слова через дефис):

# Неправильно
/api/v1/orderItems
/api/v1/order_items

# Правильно
/api/v1/order-items

Версионирование

Версия всегда идёт в пути и только в пути:

# Неправильно
/api/orders?version=1
/api/orders?v=2
/2026/orders

# Правильно
/api/v1/orders
/api/v2/orders

Минорные версии (v1.1, v1.2) не нужны — если изменение не ломает клиентов, версия не меняется. Если ломает — это v2.

Также: каждый путь должен начинаться с /api/. Это позволяет на уровне nginx или шлюза разделить API и статику без конфликтов.

ID в теле запроса вместо пути

// Неправильно — PUT с ID в теле
PUT /api/v1/orders
{ "id": "123", "status": "confirmed" }

// Правильно — ID в пути
PUT /api/v1/orders/123
{ "status": "confirmed" }

GET с побочным эффектом

GET-запросы должны быть безопасными (idempotent): их можно повторять, кэшировать, логировать без последствий. Если запрос что-то меняет — используйте POST.

# Неправильно
GET /api/v1/orders/123/cancel

# Правильно
POST /api/v1/orders/123/cancel

Query-параметры: массивы и пагинация

Массивы в query-параметрах передают повтором параметра, не через запятую:

# Неправильно
GET /api/v1/orders?status=NEW,CONFIRMED,DELIVERED

# Правильно
GET /api/v1/orders?status=NEW&status=CONFIRMED&status=DELIVERED

Пагинация с нуля сбивает пользователей API: page=0 означает «первая страница», но выглядит как «нулевая». В публичных контрактах используйте page=1 для первой страницы.

Ответы: envelope и null

Envelope — это обёртка вроде { "success": true, "data": {...} }. Она кажется удобной, но ломает стандарты: HTTP-код уже говорит об успехе или ошибке, data добавляет лишний уровень вложенности, а клиентам нужно каждый раз разворачивать обёртку.

// Неправильно — envelope
{ "success": true, "data": { "id": "123", "status": "CONFIRMED" } }

// Правильно — плоский ресурс
{ "id": "123", "status": "CONFIRMED" }

Поля со значением null лучше вовсе не включать в ответ — отсутствие поля и null несут разный смысл, и клиенты часто не умеют их различать. Пустую строку "" тоже не используют для «нет значения» — только отсутствие поля.

Ошибки

Ошибки должны возвращаться в формате RFC 9457 (Problem Details) с content-type application/problem+json:

{
  "type": "urn:problem:orders:order-not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Заказ 123 не найден"
}

Частые проблемы:

  • type: "about:blank" — бессмысленный тип-заглушка, не несёт информации
  • stack traces в ответе на 500 — утечка деталей реализации наружу; клиенту нужен code и общий detail, не трассировка стека
  • application/json вместо application/problem+json — нарушает стандарт

Заголовки и deprecation

Кастомные заголовки не нужно начинать с X- — этот префикс официально устарел. Используйте доменный префикс:

# Устаревший подход
X-Request-Id: abc123
X-Rate-Limit: 100

# Правильно
Shop-Request-Id: abc123
RateLimit-Limit: 100

Если эндпоинт помечается устаревшим — добавьте заголовок Sunset с датой отключения. Без этого клиенты не знают, когда прекратить использование.

При ограничении запросов (429 Too Many Requests) возвращайте Retry-After — клиент должен знать, когда можно повторить запрос.

Коротко

  • OpenAPI описывает весь API в одном файле — на основе него генерируются документация, SDK и тесты.
  • operationId — уникальный, camelCase, формат действие + ресурс (createOrder, confirmOrder).
  • tags — один тег на ресурс, множественное число с заглавной (Orders, Users); действия относятся к тегу родительского ресурса.
  • В OpenAPI параметры пути именуют уникально ({orderId}, {itemId}) — требование Swagger/Redoc.
  • summary — до 80 символов; description — только если логика неочевидна.
  • Глагол в URL для CRUD — ошибка; бизнес-действия выносят в конец пути (/confirm, /cancel).
  • Пути в kebab-case, версия в пути (/api/v1/), массивы — повтором параметра.
  • Envelope (success/data) не нужен — используйте плоский ресурс и HTTP-коды.
  • Ошибки возвращают в формате application/problem+json без stack traces.
  • Кастомные заголовки без X-; устаревшие эндпоинты — с заголовком Sunset.

Что почитать дальше

  • URL и ресурсы в REST — правила именования путей, вложенность, ресурсы.
  • Методы HTTP и коды ответов — когда какой метод и статус-код использовать.
  • Версионирование API — стратегии v1/v2, обратная совместимость.
  • Ошибки и Problem Details — RFC 9457 на практике.