JavaRush /Курсы /Go SELF /Документация API — примеры curl, ошибки, auth‑заголовок

Документация API — примеры curl, ошибки, auth‑заголовок

Go SELF
64 уровень , 4 лекция
Открыта

1. Документация API как часть контракта

Если тесты — это способ доказать компьютеру, что API работает как задумано, то документация — способ доказать это человеку. Причём не только пользователю API, но и вам самим через две недели, когда вы забудете, почему POST /done возвращает 204, а не 200. Документация — это не роман в трёх томах, а короткий, строгий «договор»: какие есть эндпоинты, какие заголовки обязательны, какие ответы считаются нормой и как выглядят ошибки.

Обычно документация живёт в README.md проекта, иногда в отдельном файле вроде docs/api.md. Для учебного проекта этого более чем достаточно: мы не строим портал разработчика на 40 страниц, мы строим понятный контракт, который можно проверить руками через curl и который совпадает с тем, что проверяют тесты.

Чтобы не путаться в терминах, давайте сразу договоримся о простом принципе: если поведение важно клиенту, оно должно быть описано. Если оно важно только серверу (внутренние детали), оно не должно «протекать» наружу ни в документацию, ни в ответы.

Минимальный скелет документации для нашего Task API

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

Ниже — простой скелет, который хорошо подходит почти для любого JSON API, включая наш учебный сервис задач. Я покажу его как «шаблон», который вы можете держать в README.md.

Шапка: base URL, версия, формат

Представим, что локально сервис слушает http://localhost:8080.

Параметр Значение
Base URL
http://localhost:8080
API prefix
/api/v1
Формат данных JSON (UTF-8)
Формат ошибок JSON error envelope (см. ниже)
Auth заголовок X-API-Key: <key>

Важно: даже если вы пока запускаете сервис только локально, всё равно полезно писать base URL явно. Это снижает количество «а почему у меня 404?» примерно вдвое.

2. curl‑примеры, которые работают

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

Чаще всего в примерах нам нужны четыре вещи: метод, URL, заголовки и тело запроса. На практике это выглядит так:

curl -i \
  -X POST 'http://localhost:8080/api/v1/tasks' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: dev-secret' \
  -d '{"title":"Buy milk"}'

Здесь -i просит показать статус и заголовки ответа (это удобно, потому что документация — это не только JSON‑тело, но и статус‑код). Заголовок Content-Type мы ставим, чтобы сервер точно понял, что ему прислали JSON, а не «просто текст, но мы так чувствуем».

Ещё один важный момент: в документации лучше показывать короткие команды. Если команда разрастается до 15 строк с хитрыми подстановками, читатель начинает подозревать, что это магический ритуал вызова демона, а не запрос к API.

4. Авторизация: один заголовок, один смысл, ноль сюрпризов

Авторизация в учебных API часто «прикручивается сбоку» и превращается в хаос: то нужен Authorization, то cookie, то параметр ?token=..., то «а сегодня без ключа, потому что я тестирую». Это быстро убивает и документацию, и тестируемость, и здравый смысл.

Мы фиксируем простой контракт: каждый запрос к /api/v1/... требует API‑ключ в заголовке:

  • заголовок: X-API-Key: <key>
  • если ключ не задан или неверный — клиент получает ошибку в нашем едином формате

Пример запроса без ключа (ожидаем ошибку, и это нормально):

curl -i 'http://localhost:8080/api/v1/tasks'

Пример запроса с ключом:

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks'

Важная философская часть (да, у API тоже бывает философия): наружу мы возвращаем понятное сообщение, но не раскрываем лишнего. Общая идея «не выставляйте внутренние детали как контракт» полезна для проектирования ошибок: если вы отдаёте наружу внутренности, вы случайно обещаете клиенту, что «так будет всегда», а это опасное обещание.

5. Эндпоинты нашего API: таблица + curl‑примеры

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

Таблица эндпоинтов

Метод Путь Назначение Успех
GET
/health
проверка живости сервиса
204 No Content
POST
/api/v1/tasks
создать задачу
201 Created
+ JSON задачи
GET
/api/v1/tasks
список задач (с фильтрами)
200 OK
+ JSON массив
GET
/api/v1/tasks/{id}
получить задачу по id
200 OK
+ JSON задачи
POST
/api/v1/tasks/{id}/done
пометить задачу выполненной
204 No Content
DELETE
/api/v1/tasks/{id}
удалить задачу
204 No Content

Заметьте, мы специально пишем {id} в фигурных скобках, а не «42», чтобы было видно: это параметр пути. Тесты роутинга, которые вы писали через ServeMux patterns, как раз должны подтверждать, что {id} реально извлекается и обрабатывается.

6. Примеры curl: «счастливые» сценарии

Health check

Health — это полезная штука для мониторинга и для «я вообще запустил сервер или разговариваю сам с собой?».

curl -i 'http://localhost:8080/health'
# HTTP/1.1 204 No Content

Здесь intentionally нет тела: 204 означает «всё хорошо, но контента нет».

Создание задачи

Создаём задачу Buy milk. Считайте это «Hello, world» для todo‑сервиса.

curl -i \
  -X POST 'http://localhost:8080/api/v1/tasks' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: dev-secret' \
  -d '{"title":"Buy milk"}'

Пример ожидаемого ответа (формат можно адаптировать под вашу модель, главное — чтобы он был стабилен):

{
  "id": 1,
  "title": "Buy milk",
  "done": false
}

Здесь важна не конкретная цифра 1, а то, что клиент понимает структуру: есть id, title, done. Если вы позже добавите поля, это тоже надо будет отразить в документации.

Получение списка задач

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks'

Ответ:

[
  { "id": 1, "title": "Buy milk", "done": false }
]

Если у вас поддерживаются фильтры (например, done или contains), в документации стоит показать хотя бы один пример, чтобы клиент не гадал, «а как оно задумано»:

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks?done=false'

Получение задачи по id

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks/1'

Ответ:

{ "id": 1, "title": "Buy milk", "done": false }

Пометить задачу выполненной

Мы используем отдельный endpoint /done, чтобы действие читалось как «команда» (и чтобы клиент не мучился вопросом, PATCH или POST). Возвращаем 204, потому что часто клиенту достаточно знать, что операция выполнена.

curl -i \
  -X POST \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks/1/done'
# HTTP/1.1 204 No Content

Удаление задачи

Удаление удобно делать идемпотентным по смыслу: если задачи уже нет — можно вернуть 404, а можно договориться, что всё равно 204. Но какой бы выбор вы ни сделали, он должен быть описан и проверен тестами.

Допустим, у нас договор: если задачи нет — 404. Тогда «нормальное удаление» выглядит так:

curl -i \
  -X DELETE \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks/1'
# HTTP/1.1 204 No Content

7. Ошибки как часть API: единый error envelope и таблица статусов

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

Формат (напоминаю наш контракт):

{
  "error": {
    "code": "validation",
    "message": "invalid request",
    "fields": { "title": "must not be empty" }
  }
}

Поле fields присутствует только для ошибок валидации. В остальных случаях его либо нет, либо оно пустое.

Таблица: статус → error.code → смысл

HTTP статус error.code Когда возникает
400 Bad Request
validation
неверные входные данные (в т.ч. id не число)
401 Unauthorized
unauthorized
нет X-API-Key
403 Forbidden
forbidden
ключ есть, но не подходит (если вы различаете 401/403)
404 Not Found
not_found
ресурс не найден
500 Internal Server Error
internal
ошибка на сервере, детали не раскрываем

Ключевой принцип безопасности: для 500-класса сообщение для клиента должно быть стабильным и безопасным, а детали должны уходить в логи. Этот подход — классика для Go‑мира: пользователю — понятное сообщение и корректный статус, разработчику — подробности отдельно.

И ещё одна важная мысль: если вы в одном месте возвращаете {"error":"...строка..."}, а в другом {"message":"..."}, клиент начинает писать «зоопарк парсеров», а потом ненавидеть вас (и немножко себя). Поэтому мы и держимся за единый envelope.

8. curl‑примеры ошибок и зачем их документировать

Ошибка валидации (400)

Пробуем создать задачу с пустым title:

curl -i \
  -X POST 'http://localhost:8080/api/v1/tasks' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: dev-secret' \
  -d '{"title":""}'

Ожидаем:

{
  "error": {
    "code": "validation",
    "message": "invalid request",
    "fields": { "title": "must not be empty" }
  }
}

В документации это важно по двум причинам. Во‑первых, клиент понимает, что делать (исправить поле). Во‑вторых, клиент понимает, что именно парсить: fields.title, а не «вырезать текст регуляркой из message» (регулярки в ответах — это отдельный вид страдания).

Not found (404)

Запрашиваем задачу, которой нет:

curl -i \
  -H 'X-API-Key: dev-secret' \
  'http://localhost:8080/api/v1/tasks/999'

Ответ:

{
  "error": {
    "code": "not_found",
    "message": "task not found"
  }
}

Ошибка авторизации (401/403)

Запрос без ключа:

curl -i 'http://localhost:8080/api/v1/tasks/1'

Ответ (пример):

{
  "error": {
    "code": "unauthorized",
    "message": "missing api key"
  }
}

Если вы различаете «нет ключа» и «ключ неверный», то во второй ситуации:

curl -i \
  -H 'X-API-Key: wrong-key' \
  'http://localhost:8080/api/v1/tasks/1'

Ответ (пример):

{
  "error": {
    "code": "forbidden",
    "message": "invalid api key"
  }
}

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

Internal error (500): почему нельзя «показать err.Error()» клиенту

500-ошибка в хорошем API выглядит скучно. И это комплимент. Она должна быть максимально стабильной по форме:

{
  "error": {
    "code": "internal",
    "message": "internal error"
  }
}

Если вместо этого вы возвращаете реальную причину вроде "dial tcp: connection refused", клиент начинает зависеть от текста, а вы случайно обещаете наружу детали, которые вы не хотите обещать. Рекомендация «не раскрывать внутренние детали как контракт» здесь очень практична: то, что является внутренней реализацией, не должно становиться публичным обещанием.

9. Полезные нюансы: X-Request-ID и «живая» документация

X-Request-ID в документации

Иногда кажется, что request id — это «что-то для больших микросервисов», а наш учебный сервер «и так маленький». На практике X-Request-ID — это супер‑полезная штука даже в маленьком сервисе: по нему удобно искать запрос в логах, а в тестах можно проверять, что middleware работает.

В документации достаточно одного примера: «как передать request id» и «что сервер вернёт его обратно».

curl -i \
  -H 'X-API-Key: dev-secret' \
  -H 'X-Request-ID: test-123' \
  'http://localhost:8080/api/v1/tasks'

И вы ожидаете, что в заголовках ответа будет:

X-Request-ID: test-123

С точки зрения клиента это прозрачная и полезная функциональность, а с точки зрения сервера — дисциплина, которая экономит часы отладки.

Как поддерживать документацию «живой»

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

Когда вы меняете поведение API (статус‑код, поле в JSON, формат ошибки), у вас есть три «сигнала»: интеграционные тесты, unit‑тесты и здравый смысл. Если тесты действительно проверяют контракт, то изменение контракта почти всегда приводит к изменению тестов. А раз вы уже полезли менять тесты — это хороший момент обновить и curl‑пример в README, потому что это, по сути, тот же сценарий, только для человека.

И да, это нормально, что документация иногда чуть «отстаёт». Ненормально — когда она отстаёт навсегда и превращается в фанфик по мотивам вашего API.

10. Типичные ошибки при документировании API

Ошибка №1: документация описывает «как мы сейчас сделали», а не «как клиент должен использовать».
Очень легко уйти в детали реализации: «у нас там ServeMux, потом writeJSON, потом parseID…». Клиенту это не помогает. Клиенту нужны методы, пути, заголовки, JSON, статусы и ошибки. Всё остальное — ваши внутренние приключения.

Ошибка №2: curl‑примеры нельзя запустить как есть.
Самый обидный вид плохой документации: команды красиво выглядят, но в них нет base URL, нет обязательного auth‑заголовка или забыли Content-Type. Читатель копирует, получает 401/415/400 и решает, что API не работает (а иногда — что он сам не работает, что ещё грустнее).

Ошибка №3: ошибки описаны «словами», но не описаны структурой.
Фраза «в случае ошибки вернётся JSON с сообщением» почти бесполезна. Полезно: какой HTTP статус, какой error.code, какое error.message, когда появляется fields. Если клиент пишет обработчик ошибок, он должен понимать, что парсить.

Ошибка №4: утечка внутренних ошибок в 500‑ответах.
Когда сервер возвращает наружу err.Error() для internal‑ошибки, это кажется удобным («ну зато понятно!»), но это превращает внутренности в публичный контракт и может раскрыть лишнюю информацию. Лучше держать наружу фиксированное сообщение, а детали — в логах.

Ошибка №5: нет примера авторизации, или авторизация «плавающая».
Если сегодня нужно X-API-Key, завтра «можно без него», а послезавтра «только если Луна в третьей фазе», клиентское приложение быстро превращается в набор костылей. Один заголовок, один смысл, один пример — и всем спокойнее.

1
Задача
Go SELF, 64 уровень, 4 лекция
Недоступна
Шапка контракта
Шапка контракта
1
Задача
Go SELF, 64 уровень, 4 лекция
Недоступна
Curl сценарии
Curl сценарии
1
Задача
Go SELF, 64 уровень, 4 лекция
Недоступна
Справочник ошибок
Справочник ошибок
1
Задача
Go SELF, 64 уровень, 4 лекция
Недоступна
Контракт создания
Контракт создания
1
Опрос
Тестирование HTTP, 64 уровень, 4 лекция
Недоступен
Тестирование HTTP
Тестирование HTTP
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ