JSON API Best Practices for Developers
Design better JSON APIs with consistent naming, structured responses, proper pagination, error handling, and performance optimizations used by top engineering teams.
Почему важен дизайн API
Хорошо спроектированный JSON API — это разница между интеграцией, которая занимает полдня, и той, которая занимает неделю. Хороший дизайн API снижает количество обращений в службу поддержки, делает вашу документацию короче и позволяет потребителям предсказывать, как все работает, прежде чем читать документацию.
Шаблоны в этом руководстве не теоретические — они основаны на API таких компаний, как Stripe, GitHub, Slack и других, известных отличным опытом для разработчиков. Давайте рассмотрим каждый из них.
Конвенции именования
Два доминирующих стиля для имен свойств JSON — это camelCase и snake_case. Оба варианта приемлемы, если вы выберете один и будете использовать его повсеместно.
| Конвенция | Пример | Используется | Экосистема |
|---|---|---|---|
camelCase | firstName, createdAt | Google APIs, Azure | JavaScript / TypeScript фронтенды |
snake_case | first_name, created_at | Stripe, GitHub, Slack | Python / Ruby бэкенды |
Рекомендация: Выберите конвенцию, которая соответствует вашей основной экосистеме потребителей. Если ваш API в основном используется JavaScript фронтендами, camelCase избегает трений. Если ваш бэкенд на Python или Ruby и API B2B, snake_case является идиоматичным.
Что бы вы ни выбрали, будьте абсолютно последовательны. Смешивание user_name и firstName в одном ответе разрушает доверие быстрее, чем почти что-либо другое.
Последовательная структура ответа
Каждый ответ от вашего API должен следовать предсказуемой оболочке. Вот шаблон, который работает для большинства API:
Успешный ответ
{
"data": {
"id": "usr_abc123",
"email": "alice@example.com",
"name": "Алиса Джонсон",
"created_at": "2025-01-15T09:30:00Z"
},
"meta": {
"request_id": "req_7f2a9c"
}
}
Ответ со списком
{
"data": [
{ "id": "usr_abc123", "name": "Алиса Джонсон" },
{ "id": "usr_def456", "name": "Боб Смит" }
],
"meta": {
"total": 142,
"page": 1,
"per_page": 20,
"request_id": "req_8b3d1e"
}
}
Ответ с ошибкой
{
"error": {
"code": "validation_error",
"message": "Тело запроса содержит недопустимые поля.",
"details": [
{
"field": "email",
"message": "Должен быть действительный адрес электронной почты."
},
{
"field": "age",
"message": "Должен быть не менее 13."
}
]
},
"meta": {
"request_id": "req_4c8e2a"
}
}
Ключевой принцип: data присутствует при успехе, error присутствует при неудаче, а meta появляется в обоих для отслеживания запросов и пагинации. Потребители могут написать один обработчик ответа, который сначала проверяет error.
Попробуйте сами: Вставьте ответ API в наш JSON Formatter, чтобы проверить структуру и выявить несоответствия.
Шаблоны пагинации
Большинство конечных точек списка нуждаются в пагинации. Существует три распространенных подхода, каждый из которых имеет свои плюсы и минусы:
1. Пагинация с использованием смещения
Самый простой подход. Клиент указывает номер страницы или смещение:
GET /api/users?page=3&per_page=20
{
"data": [...],
"meta": {
"total": 142,
"page": 3,
"per_page": 20,
"total_pages": 8
}
}
2. Пагинация с использованием курсора
Вместо номеров страниц сервер возвращает непрозрачный курсор, указывающий на следующий набор результатов:
GET /api/users?limit=20&after=cursor_abc123
{
"data": [...],
"meta": {
"has_more": true,
"next_cursor": "cursor_def456"
}
}
3. Пагинация с использованием ключей
Использует реальное значение столбца (например, временную метку или ID) в качестве курсора. Это пагинация с курсором без непрозрачности:
GET /api/users?limit=20&created_after=2025-01-15T09:30:00Z
{
"data": [...],
"meta": {
"has_more": true,
"next_created_after": "2025-01-16T14:22:00Z"
}
}
Сравнение пагинации
| Аспект | Смещение | Курсор | Ключи |
|---|---|---|---|
| Переход на страницу N | Да | Нет | Нет |
| Общее количество | Легко включить | Дорого | Дорого |
| Производительность на больших таблицах | Ухудшается (OFFSET сканирует строки) | Постоянная | Постоянная |
| Обработка вставок/удалений | Может пропустить или дублировать элементы | Стабильная | Стабильная |
| Сложность реализации | Низкая | Средняя | Средняя |
| Используется | Традиционные веб-приложения | Stripe, Slack | Twitter / X |
Рекомендация: Используйте пагинацию с использованием смещения для админских панелей и внутренних инструментов, где пользователи должны переходить на конкретные страницы. Используйте пагинацию с курсором или ключами для публичных API, интерфейсов бесконечной прокрутки и любых наборов данных, которые часто меняются.
Обработка ошибок
Хорошие ответы на ошибки сообщают потребителю что пошло не так, где и, желательно, как это исправить. Вот структура, которую мы рекомендуем:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Вы превысили лимит запросов в 100 запросов в минуту.",
"details": [],
"doc_url": "https://docs.example.com/errors/rate-limit"
}
}
code — это строка, читаемая машиной (не HTTP статус-код — он идет в фактическом HTTP ответе). message — читаемая человеком. Массив details предоставляет ошибки на уровне полей для неудач валидации. А doc_url ссылается на документацию об ошибке.
Распространенные коды ошибок и их соответствия HTTP статусам:
| HTTP статус | Код ошибки | Когда использовать |
|---|---|---|
| 400 | validation_error | Тело запроса не проходит валидацию схемы |
| 401 | unauthorized | Отсутствует или недействительная аутентификация |
| 403 | forbidden | Аутентифицирован, но недостаточно прав |
| 404 | not_found | Ресурс не существует |
| 409 | conflict | Дублирующий ресурс или конфликт состояния |
| 422 | unprocessable_entity | Действительный JSON, но семантически неверный |
| 429 | rate_limit_exceeded | Слишком много запросов |
| 500 | internal_error | Неожиданная ошибка сервера |
Типы данных и форматирование
Правильное определение типов данных предотвращает целую категорию ошибок интеграции:
Даты и время
Всегда используйте формат ISO 8601 с информацией о временной зоне:
{
"created_at": "2025-01-15T09:30:00Z",
"expires_at": "2025-02-15T23:59:59+05:30"
}
Никогда не используйте Unix временные метки в телах ответов — они не читаемы для человека и создают неоднозначность временной зоны. Если вам нужны Unix временные метки (например, для JWT утверждений), документируйте это четко.
Большие числа
Тип Number в JavaScript теряет точность за пределами 2^53 - 1 (9,007,199,254,740,991). Если ваши ID или денежные значения превышают это, используйте строки:
{
"id": "9223372036854775807",
"amount": "99.99",
"currency": "USD"
}
Stripe, Twitter и Discord все используют строковые ID именно по этой причине.
Null против отсутствия
Определите четкую конвенцию и документируйте ее:
null— Поле существует, но не имеет значения (например, пользователь еще не установил биографию).- Отсутствующий ключ — Поле не имеет отношения к этому ресурсу или не было запрошено (например, в частичном ответе).
Многие API включают поля null, чтобы поддерживать согласованную структуру, что облегчает клиентам написание типизированных интерфейсов без использования опциональной цепочки повсюду.
Булевы значения
Используйте настоящие булевы значения JSON, а не строки или целые числа:
// Хорошо
{ "is_active": true }
// Плохо
{ "is_active": "true" }
{ "is_active": 1 }
Стратегии версионирования
API развиваются. Вам нужна стратегия для внесения изменений без нарушения работы существующих потребителей. Три основных подхода:
- Версионирование URL:
/v1/users,/v2/users. Самый распространенный и самый заметный подход. Используется Stripe, GitHub и Twilio. Легко понять, легко маршрутизировать, но может привести к дублированию кода. - Версионирование заголовков:
Accept: application/vnd.api+json;version=2. Более чистые URL, но менее доступные. Используется GitHub (в качестве альтернативы) и Azure. - Параметр запроса:
/users?version=2. Простой, но загрязняет строку запроса. Менее распространен на практике.
Рекомендация: Версионирование URL — это прагматичный стандарт. Оно явное, кэшируемое и работает с каждым HTTP клиентом. Отклоняйтесь только в том случае, если у вас есть конкретная техническая причина.
Сжатие
JSON — это текст и отлично сжимается. Всегда включайте сжатие для ответов API:
| Алгоритм | Коэффициент сжатия | Скорость | Поддержка браузерами |
|---|---|---|---|
| gzip | Хорошо (70–80% сокращение) | Быстро | Универсальная |
| Brotli (br) | Лучше (75–85% сокращение) | Немного медленнее при сжатии | Все современные браузеры |
Для типичного JSON ответа размером 50KB, gzip уменьшает его до примерно 10–15KB, а Brotli до примерно 8–12KB. Сервер должен уважать заголовок Accept-Encoding и выбирать соответственно. Большинство обратных прокси (Nginx, Cloudflare) обрабатывают это автоматически.
Советы по производительности
Частичные ответы (Выбор полей)
Позвольте потребителям запрашивать только те поля, которые им нужны. Это уменьшает размер полезной нагрузки и нагрузку на базу данных:
GET /api/users?fields=id,name,email
{
"data": [
{ "id": "usr_abc123", "name": "Алиса", "email": "alice@example.com" }
]
}
Google APIs называют это "частичными ответами" и поддерживают это на всех конечных точках. Это особенно ценно для мобильных клиентов на медленных соединениях.
Редкие наборы полей
Для API, которые возвращают связанные ресурсы, позвольте потребителям контролировать, какие поля включены для каждого типа ресурса:
GET /api/articles?include=author&fields[article]=title,body&fields[author]=name
Это шаблон из спецификации JSON:API. Он устраняет проблему N+1 запросов на клиенте, сохраняя при этом минимальные полезные нагрузки.
ETags и условные запросы
Возвращайте заголовок ETag с ответами. Клиенты могут отправлять If-None-Match в последующих запросах, и вы возвращаете 304 Not Modified (пустое тело), если ничего не изменилось. Это экономит пропускную способность и время обработки для ресурсов, которые не часто меняются.
Контрольный список резюме
Перед отправкой вашего API пройдите через этот контрольный список:
- Последовательная конвенция именования (
camelCaseилиsnake_case) на всех конечных точках. - Предсказуемая оболочка ответа с
data,errorиmeta. - Пагинация на всех конечных точках списка с документированными ограничениями.
- Структурированные ответы на ошибки с машинно-читаемыми кодами, сообщениями и деталями на уровне полей.
- Даты ISO 8601, строковые ID для больших чисел, настоящие булевы значения.
- Стратегия версионирования API задокументирована и соблюдается.
- Включено сжатие gzip или Brotli.
- Выбор полей или редкие наборы полей для потребителей, чувствительных к производительности.
Попробуйте сами: Отформатируйте и проверьте свои ответы API с помощью нашего JSON Formatter, чтобы убедиться, что они следуют чистой, последовательной структуре.