Техническое письмо Документация API: справочник, примеры, контракт
0%

Документация API: справочник, примеры, контракт

Документация API: справочник, примеры, контракт

Половина третьего ночи, интегратор подключает ваш платёжный сервис. Он отправляет POST /v1/payments, получает таймаут на тридцатой секунде и повторяет запрос — так делают все HTTP-клиенты по умолчанию. Утром поддержка разбирает шестнадцать двойных списаний.

Интегратор не был неаккуратен. Он открыл справочную страницу и нашёл там всё: девятнадцать полей, их типы, обязательность, диапазоны, коды ответов 200, 400, 401, 404, 500. Страница сгенерирована из кода, обновляется каждым релизом, ни одного расхождения с реальностью. В ней нет одного предложения: что произойдёт, если тот же запрос придёт дважды. Ни «повтор безопасен», ни «повтор создаст второй платёж», ни «используйте Idempotency-Key». Схема этого не выражает — и генератор не выдумал.

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

Это та же оптика, что в главе «Читатель и решение»: жанр задаётся адресатом и решением, которое тот должен принять. Особенность API-документации в том, что под одним адресом /docs соседствуют три жанра, и их регулярно путают.

Границы главы. Как проектировать API — «Стили API». Как проверять соответствие реализации контракту — «Тестирование API». Как собирать требования к интеграции — «Анализ API». Как защищать«Безопасность API». Здесь — про текст: что написать, чем держать его в актуальном состоянии и как отличить документацию от её имитации.

Три документа под одним именем

Слой Читатель Его решение Источник правды Кто пишет
Справочник пишет код прямо сейчас какое поле подставить, что значит код схема и типы в коде генератор
Примеры оценивает продукт за 15 минут стоит ли тратить неделю исполняемый код инженер, редко
Контракт строит систему поверх вашей на какие гарантии опереться головы трёх человек никто

Последняя ячейка — источник большинства инцидентов интеграции. Справочник генерируется и потому всегда есть. Примеры пишутся один раз при запуске и потом тихо гниют. Контракт не пишется вообще, потому что не следует из кода механически: его надо вспомнить, сформулировать и решиться зафиксировать. Зафиксировать страшно — записанное становится обязательством.

Три слоя документации API и три читателя

Практический тест на зрелость: откройте документацию своего сервиса и найдите ответ на три вопроса — «безопасно ли повторить этот запрос», «в каком порядке придут события», «что будет при превышении лимита». Справочник полон, а этих ответов нет — у вас не документация, а её самая дешёвая треть.

Справочник: читатель уже внутри

Справочник не читают — в него попадают: из стектрейса, из ответа с незнакомым кодом, из автодополнения в IDE. Читатель приходит с одним конкретным словом и хочет уйти через сорок секунд. Отсюда требования к форме: адресуемость (у каждого поля и кода ошибки свой якорь, на который можно сослаться в тикете), единообразие (одинаковое описано одинаковыми словами в одинаковом порядке — читатель сканирует, а не читает), полнота важнее красоты, никакого повествования.

Главная содержательная ошибка — описание, пересказывающее имя поля.

<!-- Плохо: описание повторяет имя -->
customer_id (string, required) — идентификатор клиента.
amount (integer, required) — сумма.
status (string) — статус платежа.

Полезное описание отвечает на два вопроса, которых в схеме нет: откуда я это возьму и что будет, если значение неверное.

<!-- Переписано -->
customer_id (string, required) — идентификатор клиента в биллинге, вида `cus_` + 24 символа.
    Возвращается при создании клиента (`POST /v1/customers`) и не меняется никогда.
    Клиента нет или он удалён → 404, `code: "customer_not_found"`.

amount (integer, required) — сумма в минорных единицах валюты: 4900 = 49,00 RUB.
    Дробные значения отклоняются с 400 `amount_not_integer` — самая частая ошибка интеграции.
    Максимум 100 000 000 (миллион рублей) на операцию.

status (string, read-only) — состояние платежа: pending, authorized, captured, canceled,
    refunded, partially_refunded. Переходы — в разделе «Жизненный цикл платежа»; поле
    меняется асинхронно, опрашивать его вместо подписки на события не нужно.

Разница не в объёме, а в том, что второй вариант закрывает решения читателя: где взять, в чём мерить, что сломается. Одно предложение про минорные единицы снимает целый класс инцидентов, где интегратор списывает в сто раз больше.

Язык справочника — по правилам главы «Ясность»: никаких «может быть», «как правило», «обычно». Либо поле обязательное, либо нет; либо порядок гарантирован, либо не гарантирован. Модальность здесь — невыполненная работа автора, переложенная на читателя. И описания живут прямо в схеме, а не в документе рядом с ней:

# openapi.yaml — фрагмент, из которого собирается справочник
components:
  schemas:
    PaymentRequest:
      type: object
      required: [customer_id, amount, currency]
      properties:
        customer_id:
          type: string
          pattern: '^cus_[A-Za-z0-9]{24}$'
          description: >
            Идентификатор клиента в биллинге. Возвращается при создании клиента
            (POST /v1/customers) и неизменен. Нет такого клиента → 404 customer_not_found.            
          example: cus_9Kd2mQ7fT4pLxA0vB3nR8sZ
        amount:
          type: integer
          minimum: 1
          maximum: 100000000
          description: >
            Сумма в минорных единицах: 4900 = 49,00 RUB.
            Дробное значение → 400 amount_not_integer.            
          example: 4900

Справочник обязан генерироваться

Рукописный справочник расходится с кодом молча: код меняется в одном файле, текст живёт в другом, ничто их не связывает. Через полгода расхождение уже не найти иначе как поштучным сравнением. Лекарство — не дисциплина, а единственный источник правды плюс проверка на пути в прод.

Подход Источник правды За Против
Code-first аннотации и типы в коде справочник физически не может отстать спека уродлива, дизайн API рождается стихийно
Spec-first openapi.yaml в репозитории API обсуждают до реализации, клиенты генерируются заранее код может разойтись со спекой, нужна отдельная проверка

Оба работают. Не работает третий — «спека в вики, код живёт своей жизнью». Выбор между первыми двумя — это ADR, а не вкусовщина: он определяет, кто и когда правит контракт.

Три проверки в середине стоят полдня настройки и снимают три разных класса лжи. Линтер спецификации ловит пустоту: метод без описания, поле без примера, отсутствующий 4xx — без него генерация даёт формально полный и содержательно пустой справочник, «user_id — the user id» на каждой строке. Диффер ловит ломающее изменение до того, как его увидит клиент: удалённое поле, сузившийся enum, ставший обязательным параметр. Проверка соответствия ловит расхождение реализации со спекой.

# .spectral.yaml — минимум, который стоит включить в первый же день
extends: ["spectral:oas"]
rules:
  operation-description-required:
    description: У каждой операции есть описание длиннее 40 символов
    given: "$.paths[*][get,post,put,patch,delete]"
    severity: error
    then:
      field: description
      function: length
      functionOptions:
        min: 40
  property-description-required:
    description: У каждого свойства схемы есть описание
    given: "$.components.schemas[*].properties[*]"
    severity: error
    then:
      field: description
      function: truthy
# в пайплайне: любая из трёх проверок красная — PR не едет
spectral lint openapi.yaml --fail-severity=error
oasdiff breaking origin/main:openapi.yaml openapi.yaml --fail-on ERR
schemathesis run --checks all --base-url "$STAGING_URL" openapi.yaml

Честная оговорка: генерация даёт полноту и синхронность, но не смысл. Она перенесёт ровно то, что вы написали в аннотациях; если там пусто, вы получите идеально актуальный бесполезный документ. Генератор снимает вопрос «расходится ли», но не вопрос «сказано ли хоть что-нибудь».

Примеры: единица доверия

Пример — единственная часть документации, которую выполняют, а не читают. Не запустился с первого раза — доверие падает ко всему тексту сразу, включая справочник.

# Плохо: не запускается ни у кого
curl -X POST https://api.example.com/v1/payments \
  -H "Authorization: Bearer <your-token>" \
  -d '{ "customer_id": "...", "amount": ... }'

Плейсхолдеры вместо значений; нет Content-Type (сервер вернёт 415, и читатель решит, что сломан ваш API); не показан ответ; не сказано, где взять токен; ни слова про ошибку.

# Переписано: копируется и работает
# Тестовый ключ — в консоли, Developers → API keys. Ключи sk_test_* работают
# на песочнице и не двигают реальные деньги.
export API_KEY="sk_test_4eC39HqLyjWDarjtT1zdp7dc"

curl -X POST https://api.example.com/v1/payments \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3a1c9e-5b2d-4a11-9f70-2c8e6d4b1a05" \
  -d '{"customer_id": "cus_9Kd2mQ7fT4pLxA0vB3nR8sZ", "amount": 4900, "currency": "RUB"}'
// 201 Created
{ "id": "pay_3Nk8QeLm2vA7", "status": "authorized", "amount": 4900,
  "currency": "RUB", "capture_before": "2026-03-19T09:41:07Z" }

// Тот же запрос с картой, которую банк отклоняет (номер 4000 0000 0000 0002) — 402
{ "type": "https://api.example.com/errors/card_declined",
  "title": "Карта отклонена банком-эмитентом", "status": 402,
  "code": "card_declined", "decline_reason": "insufficient_funds",
  "retriable": false, "payment_id": "pay_3Nk8QeLm2vB1" }

Три вещи отличают рабочий раздел примеров от декоративного.

  1. Пример на ошибку важнее примера на успех. Happy path интегратор пройдёт сам за двадцать минут; неделю он потеряет на разборе того, что вернулось в четверг вечером. Дайте способ воспроизвести отказ намеренно: тестовые карты, специальные суммы, заголовок X-Simulate-Failure.
  2. Пример показывает сценарий, а не один вызов. «Создать клиента → привязать способ оплаты → списать → вернуть» — это то, что нужно на самом деле; четыре изолированных вызова читатель и так видит в справочнике.
  3. Примеры выполняются в CI. Иначе они устаревают быстрее всего остального: в них зашиты не только имена полей, но и значения, ключи, URL и порядок шагов.

Последний пункт — главное структурное решение раздела. Пример живёт как код, а в документ попадает включением:

# tests/docs/test_quickstart.py — тот самый пример из документации, но как тест
import os, uuid, httpx, pytest

client = httpx.Client(
    base_url=os.environ["SANDBOX_URL"],
    headers={"Authorization": f"Bearer {os.environ['SANDBOX_KEY']}"},
    timeout=10.0,
)

def test_quickstart_creates_authorized_payment() -> None:
    """Шаг 2 быстрого старта: создание платежа возвращает authorized."""
    r = client.post("/v1/payments",
                    headers={"Idempotency-Key": str(uuid.uuid4())},
                    json={"customer_id": CUSTOMER_ID, "amount": 4900, "currency": "RUB"})
    assert r.status_code == 201
    body = r.json()
    # Проверяем ровно те поля, которые обещаны в примере ответа: перестанет отдавать —
    # упадёт наш тест, а не интеграция читателя.
    assert body["status"] == "authorized" and body["amount"] == 4900
    assert body["id"].startswith("pay_") and "capture_before" in body

def test_declined_card_returns_documented_error() -> None:
    """Раздел «Обработка отказов»: код ошибки и retriable — часть контракта."""
    r = client.post("/v1/payments",
                    headers={"Idempotency-Key": str(uuid.uuid4())},
                    json={"customer_id": DECLINE_CUSTOMER_ID, "amount": 4900, "currency": "RUB"})
    assert r.status_code == 402
    assert r.json()["code"] == "card_declined"   # код стабилен, текст title — нет
    assert r.json()["retriable"] is False

Цена — один прогон в CI. Выгода — примеры перестают быть обещаниями и становятся проверяемыми утверждениями. Тот же принцип, что в главе «Ревью текста»: непроверяемое утверждение рано или поздно окажется ложным.

Контракт: то, чего нет в схеме

Схема описывает форму сообщений, контракт — поведение системы. Ни один генератор не выведет поведение из типов, потому что его там нет. Вот что произошло в ночной сцене из начала главы:

Шаг 7 — единственный, который справочник описать не может. Полный перечень того, что относится к контракту и пишется только руками:

Гарантия Вопрос читателя Если не написано
Идемпотентность и ретраи безопасно ли повторить? двойные списания
Порядок и доставка событий по порядку? могут ли дублироваться? рассинхрон состояний
Согласованность увижу ли в GET то, что только что записал? «баг: данные не сохранились»
Пагинация стабилен ли курсор при вставках? пропуски и дубли при выгрузке
Лимиты что при превышении, лимит на ключ или на организацию? ночная выгрузка кладёт интеграцию
Коды ошибок что перебирать в switch, что стабильно? парсинг человеческого текста
Стабильность формата что появится и исчезнет без смены версии? клиент падает на новом поле
Единицы и время минорные единицы? UTC? граница включена? ошибки в сто раз и на сутки

Типичный абзац про идемпотентность выглядит так:

<!-- Плохо -->
Метод является идемпотентным. При возникновении сетевых ошибок запрос можно
безопасно повторить. Рекомендуется использовать заголовок Idempotency-Key.

Три слова делают абзац бесполезным: «является» (в каком смысле?), «рекомендуется» (а если нет?), «безопасно» (в течение какого времени?). После него читатель знает столько же, сколько до, но думает, что знает больше, — это хуже молчания.

<!-- Переписано -->
## Повторные запросы

POST и DELETE идемпотентны **только при передаче Idempotency-Key**. Без заголовка повтор
создаёт вторую операцию — это не ошибка, а поведение по умолчанию.

- Ключ — строка до 255 символов, уникальная для операции. Берите UUID v4 и сохраняйте
  его у себя **до** отправки: после таймаута воспроизвести тот же ключ будет нечем.
- Первый запрос с ключом выполняется, ответ сохраняется на 24 часа.
- Повтор с тем же ключом и тем же телом возвращает сохранённый ответ с исходным
  HTTP-кодом и заголовком `Idempotent-Replay: true`. Побочных эффектов нет.
- Повтор с тем же ключом и **другим** телом → 409, `idempotency_key_reuse`
  (тело сравнивается по хешу, порядок полей JSON не важен).
- Повтор, пришедший пока первый ещё выполняется, → 409, `request_in_flight`.
- Через 24 часа ключ забывается, и тот же запрос создаст вторую операцию. Для сверки
  за пределами суток используйте свой `external_id` (см. «Дедупликация»).

Наш таймаут — 30 с. Ответа нет → повторяйте **с тем же ключом**, максимум 5 раз
с задержкой 1, 2, 4, 8, 16 с. Статус так и неизвестен → `GET /v1/payments?idempotency_key=...`
вернёт операцию, если она была создана.

Абзац стал в пять раз длиннее, и здесь длина оправдана: он экономит инцидент. Это принцип из главы «Структура» — объём выделяется не по важности темы для автора, а по цене ошибки читателя.

Второй пример — лимиты, где почти все пишут одну и ту же пустую фразу.

<!-- Плохо -->
API имеет ограничение на количество запросов. При превышении лимита возвращается
ошибка 429. Лимиты могут быть изменены.

<!-- Переписано -->
Лимит — 100 запросов в секунду на организацию (не на ключ: все ваши ключи делят один
бюджет), скользящее окно 1 с. Пакетный `POST /v1/payments/batch` считается за N запросов
по числу элементов. Превышение → 429 с заголовками:
  Retry-After: 2            — через сколько секунд повторять
  RateLimit-Remaining: 0    — остаток в текущем окне
  RateLimit-Reset: 2        — когда окно обнулится
429 не расходует идемпотентный ключ: повтор с тем же ключом корректен.
Нужен лимит выше — заявка в поддержку, рассматриваем 3 рабочих дня.
Снижение лимитов анонсируется за 90 дней (см. «Политика изменений»).

Коды ошибок — часть контракта не меньше, чем успешный ответ. Самая ценная их часть — машиночитаемый code, по которому клиент ветвится; человекочитаемый title меняться будет.

| code | HTTP | Повторять? | Что делать |
|---|---|---|---|
| card_declined | 402 | нет | показать причину из decline_reason |
| customer_not_found | 404 | нет | проверить customer_id, ошибка интеграции |
| idempotency_key_reuse | 409 | нет | ключ переиспользован на вашей стороне |
| request_in_flight | 409 | да, через 1–2 с | тот же ключ, экспоненциальная задержка |
| rate_limited | 429 | да, по Retry-After | снизить параллелизм |
| provider_unavailable | 503 | да, до 5 раз | тот же ключ, задержка 1, 2, 4, 8, 16 с |

Колонка «Повторять?» напрямую отображается в if в коде читателя — тот случай, когда одна колонка таблицы стоит трёх страниц текста. Формат тела ошибки лучше взять стандартный — RFC 9457 «Problem Details for HTTP APIs» (https://www.rfc-editor.org/rfc/rfc9457.html), чтобы читателю не пришлось изучать вашу персональную структуру.

Третье, чего нет в схеме, — жизненный цикл ресурса. Справочник показывает поле status и перечисление значений; он не показывает, какие переходы возможны, какие необратимы и где окно для действия.

Диаграмма отвечает на вопрос «когда ещё не поздно отменить», который иначе разбирается в переписке с поддержкой. Про то, когда схема помогает, а когда подменяет отсутствующий текст, — глава «Схемы в документах».

Теория под всем разделом — «Идемпотентность и доставка» и «Модели согласованности». Задача автора — не пересказать теорию, а сказать, какой вариант выбран у вас, в одном абзаце и с числами.

Депрекейт — отдельный жанр

Изменение API — это документ, а не только код. Читатель принимает решение: затронут ли я, что менять, к какому сроку. Типичное сообщение не даёт ни одного из трёх ответов.

<!-- Плохо -->
Метод `GET /v1/payments/list` устарел. Рекомендуется использовать новый метод.

<!-- Переписано -->
## `GET /v1/payments/list` объявлен устаревшим 12.03.2026

**Отключение — 15.09.2026.** После этой даты метод вернёт 410, `code: "endpoint_sunset"`.

**Затронуты вы, если** в логах есть вызовы этого пути или ответы приходят с заголовком
`Deprecation: Wed, 12 Mar 2026 00:00:00 GMT`. Проверить свой трафик:
`GET /v1/usage/deprecated?from=2026-03-01` — вернёт вызовы устаревших методов по вашим ключам.

**Что делать:** перейти на `GET /v1/payments`.

| Было (`/list`) | Стало (`/payments`) | Почему |
|---|---|---|
| `offset` / `limit` | `cursor` / `limit` | offset-пагинация пропускала записи при вставках |
| `date_from` (дата) | `created_after` (RFC 3339) | границы однозначны, часовой пояс обязателен |
| `items` | `data` | единый конверт списка во всех методах |
| нет | `has_more` | признак следующей страницы |

Полей стало больше, ни одно не исчезло, порядок сортировки прежний.
Типичная миграция — 1–2 часа. Вопросы: #api-migrations.

Ключевая часть тут структурная, а не текстовая: сигнал в самом протоколе — заголовки Deprecation (RFC 9745, https://www.rfc-editor.org/rfc/rfc9745.html) и Sunset (RFC 8594, https://www.rfc-editor.org/rfc/rfc8594.html) в ответах устаревшего метода плюс отчёт по использованию. Тогда до читателя доходит не рассылка, которую он не читает, а данные в его собственных логах, а у вас появляется честный RFC на отключение с цифрами: «осталось семь клиентов, 94 запроса в сутки». Журнал изменений при этом пишется не списком коммитов, а в терминах «что вам придётся сделать»: ломающее, новое, исправленное. Про запись изменений в терминах читателя — «Совместная работа в Git».

Не только REST

Три слоя переносятся на любой стиль, меняется техника генерации справочника.

  • gRPC / Protobuf. Комментарии в .proto — и есть справочник, они уезжают в сгенерированные клиенты и всплывают в IDE; ломающие изменения ловит buf breaking. Контрактный слой тот же: дедлайны, ретраи, идемпотентность, семантика UNAVAILABLE против FAILED_PRECONDITION.
  • GraphQL. Описания живут в SDL, @deprecated(reason: "...") — обязательное поле, а не украшение: reason — это ваш абзац миграции в одну строку. Специфический вопрос контракта — лимиты по сложности запроса, а не по числу вызовов.
  • События и очереди. AsyncAPI описывает схему сообщения; всё остальное — контракт: at-least-once или exactly-once, ключ партиционирования, гарантии порядка, что делать с дублями, время жизни в очереди, куда попадает необработанное. Здесь контрактный слой доминирует над справочным.
  • Внутренние API. Соблазн «мы одна компания, спросят в чате» работает, пока команд три. Внутренние API отличаются от публичных не наличием документации, а тем, что контрактный слой у них важнее справочного: соседи прочитают ваш код, но не узнают из него, на что вы готовы дать обещание. На масштабе платформы — «Инфраструктурное API».

Почему документация устаревает и что делать структурно

Призывы «обновляйте документацию» не работают нигде по простой причине: устаревание — следствие не лени, а расстояния между документом и изменением. Чем больше шагов между «поменял код» и «поменял текст», тем вероятнее, что второй шаг не случится. Лечится это сокращением расстояния и превращением расхождения в поломку сборки.

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

Правый верхний угол — самый опасный: из кода не выводится, меняется постоянно. Там живут скриншоты и пошаговые сценарии, и там документация гниёт первой. Дешёвое решение — сокращать этот квадрант: скриншоты заменять текстом, сценарии — исполняемыми примерами, остаток снабжать владельцем и сроком.

Пять механизмов, которые работают вместо призывов:

  1. Близость. Текст лежит в том же репозитории и правится в том же PR, что и код. Документация в отдельной вики отстаёт всегда — не потому, что о ней забывают, а потому, что для её правки нужен отдельный акт воли. Правило: изменение контракта без изменения текста не проходит ревью, это часть критериев приёмки.
  2. Генерация. Всё, что выводится из типов, выводится из типов. Рукописный список полей — гарантированная будущая ложь.
  3. Проверяемость. Расхождение обязано ломать сборку: линтер спеки, диффер ломающих изменений, примеры как тесты, контрактные тесты со стороны потребителя (Pact). Документ, расхождение которого с реальностью ничего не ломает, с реальностью разойдётся.
  4. Владелец. У каждой рукописной страницы — команда в CODEOWNERS, а не «отдел документации». Владелец не тот, кто пишет, а тот, кому приходит ревью при изменении.
  5. Срок жизни. У страниц контрактного слоя — дата ревизии и напоминание раз в полгода: подтвердить или удалить. Устаревшая страница хуже отсутствующей: отсутствие заставляет спросить, ложь — действовать уверенно и неправильно. Удаление — нормальная операция с документом, а не признание поражения.

Шестой механизм — обратная связь: повторяющийся вопрос в канале интеграции заводится как баг документации с той же приоритизацией. Три одинаковых вопроса — это не три непонятливых читателя, это одна отсутствующая страница. Аналитика поиска по порталу (что искали и не нашли) — самый дешёвый источник задач, который почти никто не смотрит.

Часть документов пишется ради процесса

Честная часть главы. Некоторые документы про API существуют не для читателя, а для регламента, и признать это полезнее, чем делать вид, что они кому-то помогают. Выглядит это так: выгрузка Swagger в Word раз в квартал как приложение к акту сдачи этапа; раздел «Описание интерфейсов взаимодействия» из требований к комплекту документации; «протокол информационного обмена», подписанный двумя департаментами и с тех пор не открывавшийся; страница в вики, созданная под аудит и заполненная копипастой.

Четыре теста распознают такой документ за пять минут:

  • Тест удаления. Удалите страницу — кто заметит и через сколько? Никто и никогда — документ процессный.
  • Тест пути. Как читатель на неё попадает? Единственный путь — ссылка из письма о сдаче этапа? Значит, читателей нет.
  • Тест последнего изменения. Правки только перед вехами проекта и ни одной по ходу разработки — документ обслуживает веху, а не интеграцию.
  • Тест вопроса. Задайте документу вопрос, который реально возник у интегратора на прошлой неделе. Ответа нет — документ не про интеграцию.

Что делать. Не воевать: требование чаще всего законно. У аудитора и приёмочной комиссии действительно есть решение, которое они принимают по этому документу («принимаем этап», «система соответствует»), — это другой читатель, а не отсутствие читателя, и мерить такой документ метриками интегратора бессмысленно. Разумная стратегия — разделить слои и автоматизировать процессный: пусть он генерируется из той же спецификации одной командой в CI, тогда он стоит минуты, а не недели. И ни при каких обстоятельствах не давайте процессной копии стать источником правды: как только правки начинают вноситься в Word, живой слой умирает. Если решение приходится защищать наверху, аргумент — не «так правильно», а стоимость: часы в квартал на ручную выгрузку против одной команды в пайплайне («Работа вверх»).

И обратная честность: если ваш «живой» слой проходит те же четыре теста как процессный — никто не заметит удаления, никто туда не ходит, правки только перед релизом, — значит, вы тоже пишете ради процесса, просто ваш процесс называется «у взрослых команд есть документация». Лучше одна честная страница, которую читают, чем двадцать, которые не читает никто.

Типичные ошибки

  • Считать, что Swagger UI — это документация. Это справочник, треть работы, а без описаний — пустая треть.
  • Описание, повторяющее имя поля. Стоит ровно ноль и создаёт иллюзию заполненности.
  • Примеры с плейсхолдерами. <your-token>, ..., PUT_YOUR_ID_HERE — читатель не может проверить, работает ли ваш API, и уходит.
  • Только happy path. Интегратор напишет свою эвристику по текстам сообщений, и она сломается на первом же релизе.
  • Человекочитаемый текст ошибки как контракт. Как только клиенты начнут ветвиться по title, вы не сможете исправить в нём опечатку.
  • Модальность в контракте. «Обычно события приходят по порядку» — не гарантия, а ловушка. Либо гарантируете, либо пишете, что порядок не гарантирован.
  • Документация в вики отдельно от кода. Расстояние гарантирует расхождение.
  • Депрекейт без даты и пути миграции. Переносят не слова «используйте новый метод», а дата, отчёт по своему трафику и таблица соответствия полей.
  • Молчание про лимиты и таймауты. Их всё равно узнают — на проде, ночью.
  • Тон, объясняющий читателю, как ему жить. Документация API — такой же интерфейс, и правила из главы «Текст в интерфейсе» здесь работают целиком.

Мини-итог

  • Документация API — три жанра под одним адресом: справочник для того, кто уже внутри; примеры для того, кто решает, входить ли; контракт для того, кто строит поверх вас.
  • Справочник обязан генерироваться из единственного источника правды. Генерация даёт синхронность, но не смысл; смысл добавляют описания, которых требует линтер.
  • Полезное описание поля отвечает на «откуда взять» и «что будет, если неверно».
  • Пример — единственная часть, которую выполняют: копируется целиком, показывает ответ, включает случай ошибки и живёт в CI как тест.
  • Контракт — то, чего нет в схеме: идемпотентность, порядок, согласованность, лимиты, коды ошибок, стабильность формата, единицы и время. Пишется руками, с числами, без «обычно» и «рекомендуется».
  • Депрекейт — жанр с обязательными полями: дата отключения, способ узнать, что вы затронуты, таблица соответствия, заголовки Deprecation и Sunset в ответе.
  • Устаревание лечится структурой, а не призывами: близость к коду, генерация, проверяемость в CI, владелец, срок жизни и удаление как нормальная операция.
  • Часть документов пишется ради процесса. Распознаётся тестами удаления, пути, последнего изменения и вопроса; лечится разделением слоёв и автоматизацией процессного.

Источники

Что дальше

Справочник, примеры и контракт отвечают на вопросы человека, который уже решил работать с вашей системой. Между «скопировал curl» и «понимаю, как здесь всё устроено» лежит пространство обучающих текстов, и оно устроено по другим правилам. Быстрый старт, руководство по задаче, объяснение концепции и справочник — четыре разных жанра, которые регулярно смешивают в один документ, и от этого не работает ни один из четырёх.

Руководства и обучающие тексты: разные жанры, разные правила

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов