Какую структуру должен использовать ответ API
Главное: Голые объекты чисты, пока не нужны метаданные; полные оболочки безопасны, но многословны. Для большинства проектов используйте HTTP-статус для результата и стабильную оболочку для данных.

Главное: выражайте результат через коды статуса 4xx/5xx, несите данные в { "data": ..., "meta": ... }, а при ошибке переключайтесь на оболочку error; этого достаточно для внутренних API, и только публичные платформы со множеством интеграторов нуждаются в JSON:API или OpenAPI.
Сначала исключите худший вариант: всегда возвращать 200 с результатом в поле code.
Три формы
A. Голый объект
{ "id": 1, "name": "demo", "price": 99 }
Чисто, и семантику выполняет HTTP (404 — это 404). Но в момент, когда вам нужны пагинация, предупреждения или trace id, их некуда поместить без изменения контракта.
B. Стабильная оболочка
{
"data": { "id": 1, "name": "demo" },
"meta": { "requestId": "abc", "took": 12 }
}
Данные и метаданные живут отдельно, поэтому новые метаданные никогда не перестраивают data. Цена — один уровень вложенности.
C. Всё в оболочке (избегайте)
{ "code": 0, "message": "success", "data": {...} }
Порок не в форме, а в практике. Статус 200 для любого результата означает, что CDN, шлюзы, автоматические выключатели, политики повторов и дашборды видят успех. Затем каждое оповещение требует собственного парсинга.
Рекомендация
HTTP-статус для результата, оболочка для данных.
{
"data": { "id": 1, "name": "demo" },
"meta": { "requestId": "req_8f2a", "took": 12 }
}
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order does not exist or was deleted",
"details": [{ "field": "id", "issue": "not_found" }]
},
"meta": { "requestId": "req_8f2a" }
}
Статус 4xx или 5xx; code стабильный и машиночитаемый; message для людей; details для ошибок полей формы.
Жёсткие правила
- Конечные точки списков должны нести метаданные пагинации. Без
totalили курсора нет постраничного просмотра. - Пустые списки возвращают
[], никогдаnull.null.map— повторяющийся сбой. - Метки времени в ISO 8601 со смещением. См. в каком формате метки времени.
- Одно соглашение об именовании.
snake_caseилиcamelCase, конвертированное один раз на границе. - Деньги как целые минорные единицы или строки.
99.99неточно в двоичной плавающей точке. - Не смешивайте
nullс отсутствием. См.nullили опустить поле.
Когда принимать спецификацию
- Публичные платформы со множеством интеграторов: JSON:API или OpenAPI плюс общая оболочка
- Внутренние API одной команды: оболочки выше достаточно
- Строгие контракты: Schema сначала, структуры генерируются из неё
Спецификации экономят обсуждения и стоят онбординга. Для команды из пяти человек JSON:API обычно — отрицательная работа.
Вопросы и ответы
Как выбрать между голым объектом и оболочкой?
Используйте оболочку, когда нужны пагинация, предупреждения или requestId, чтобы meta мог расти, не перестраивая data. Возвращайте голый объект только когда метаданные не нужны и вы можете принять, что добавление полей позже изменит контракт.
Почему API никогда не должен возвращать 200 для любого результата?
CDN, шлюзы, автоматические выключатели, политики повторов и дашборды читают HTTP-статус, чтобы решить, кешировать, повторять или оповещать. С 200 везде они считают сбои успехами, и каждое оповещение требует собственного парсинга бизнес-кода.
Должны ли конечные точки списков нести метаданные пагинации?
Да. Без total или курсора нет постраничного просмотра, а пустые списки должны возвращать [], а не null, иначе вызов .map во фронтенде ломается.