Плохой API — это боль. Ты интегрируешь чужой сервис, а там: /getUser, /fetchUserData, /user_info — три эндпоинта для одного и того же, документация отсутствует, коды ошибок придуманы на ходу. Знакомо? Чтобы твой API не стал таким кошмаром для других разработчиков, стоит разобраться с базовыми принципами проектирования.
Ресурсы, а не действия
Главная идея REST — работать с ресурсами, а не с командами. Ресурс — это сущность: пользователь, заказ, статья. URL должен описывать, с чем ты работаешь, а HTTP-метод — что ты с этим делаешь.
Плохо:
GET /getUsers
POST /createUser
DELETE /deleteUser?id=5
Хорошо:
GET /users — список пользователей
POST /users — создать пользователя
GET /users/5 — конкретный пользователь
PUT /users/5 — обновить полностью
PATCH /users/5 — обновить частично
DELETE /users/5 — удалить
URL — существительное, метод — глагол. Это правило избавляет от половины споров при проектировании.
Вложенные ресурсы используй там, где есть реальная иерархия:
GET /users/5/orders — заказы пользователя
GET /users/5/orders/12 — конкретный заказ
Но не увлекайся. Вложенность глубже двух уровней делает URL громоздким и сложным в поддержке. Если нужно /companies/3/departments/7/employees/42/tasks — это сигнал пересмотреть структуру.
Именование: единообразие важнее правил
Можно использовать snake_case (/user_orders) или kebab-case (/user-orders) — главное делать это везде одинаково. Большинство популярных API (GitHub, Stripe, Twilio) используют kebab-case в URL и snake_case в теле запроса. Это разумный выбор.
Несколько конкретных правил:
- Существительные во множественном числе:
/users, /orders, /products
- Только строчные буквы
- Никаких глаголов в URL, кроме специальных операций (об этом ниже)
- Никаких расширений:
/users.json — архаика
Исключение — нестандартные операции, которые сложно выразить через CRUD. Например, блокировка аккаунта или отправка уведомления:
POST /users/5/block
POST /orders/12/cancel
POST /notifications/send
Это допустимо. Главное — документировать такие эндпоинты явно.
HTTP-методы и статусы — используй их по назначению
HTTP придумали не просто так. Статусы несут смысл, и их правильное использование позволяет клиентам реагировать на ответы без парсинга тела:
2xx — успех:
200 OK — стандартный успех для GET, PUT, PATCH
201 Created — ресурс создан (POST), обязательно верни Location с URL нового ресурса
204 No Content — успешное удаление или обновление без тела ответа
4xx — ошибка клиента:
400 Bad Request — невалидные данные в запросе
401 Unauthorized — не аутентифицирован
403 Forbidden — аутентифицирован, но нет прав
404 Not Found — ресурс не найден
409 Conflict — конфликт (например, email уже занят)
422 Unprocessable Entity — данные валидны по формату, но логически некорректны
429 Too Many Requests — превышен rate limit
5xx — ошибка сервера:
500 Internal Server Error — что-то сломалось на сервере
503 Service Unavailable — сервис временно недоступен
Ошибка, которую делают почти все новички: возвращать 200 OK с {"success": false} в теле. Это ломает всю логику обработки ошибок на стороне клиента.
Формат ошибок
Ошибки должны быть информативными и единообразными. Хороший формат:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "age",
"message": "Must be at least 18"
}
]
}
}
Машиночитаемый code позволяет клиенту программно обрабатывать ошибку. message — для разработчика. details — для валидации форм, чтобы подсветить конкретные поля.
Не возвращай stack trace в продакшне — это утечка информации.
Версионирование
API меняется. Если ты не версионируешь API с самого начала, первый же breaking change сломает всех клиентов.
Есть несколько подходов:
В URL (самый распространённый):
/v1/users
/v2/users
Просто, явно, легко тестировать. Именно так делают Stripe, Twilio, большинство крупных API.
В заголовке:
Accept: application/vnd.api+json; version=2
Чище с точки зрения REST, но неудобно тестировать через браузер.
В параметре запроса:
/users?version=2
Плохой вариант — засоряет URL и легко потерять.
Рекомендация: используй версию в URL. Это не идеально с академической точки зрения, но работает на практике. Начни с /v1 сразу, даже если думаешь, что никогда не будет v2. Будет.
При выходе новой версии поддерживай старую минимум 6-12 месяцев. Уведоми клиентов заранее через заголовок Deprecation: true и Sunset: <дата>.
Пагинация и фильтрация
Возвращать тысячи записей в одном ответе — путь к таймаутам и упавшим мобильным приложениям. Пагинация обязательна для любого списочного эндпоинта.
Offset-based (классика):
GET /users?page=2&per_page=20
Просто реализовать, легко понять. Минус: при вставке новых записей страницы «съезжают».
Cursor-based (для больших данных):
GET /users?cursor=eyJpZCI6MTAwfQ&limit=20
Стабильно при изменениях данных, хорошо работает с реальным временем. Используют Twitter, Facebook. Минус: нельзя перейти на произвольную страницу.
В ответе всегда отдавай метаданные:
{
"data": [...],
"pagination": {
"total": 1543,
"page": 2,
"per_page": 20,
"total_pages": 78
}
}
Фильтрация через параметры запроса:
GET /orders?status=pending&created_after=2026-01-01&sort=created_at&order=desc
Сортировку делай явной и документированной. Клиент не должен гадать, по какому полю сортируется список по умолчанию.
Аутентификация и безопасность
Для большинства API сегодня стандарт — JWT или API-ключи через заголовок Authorization:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Несколько важных моментов:
HTTPS везде. Без исключений. Передача токенов по HTTP в 2026 году — это просто отдать ключи злоумышленнику.
Rate limiting. Ограничивай количество запросов. Возвращай заголовки X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. При превышении — 429 Too Many Requests.
Не передавай чувствительные данные в URL. Токены, пароли, личные данные — только в теле или заголовках. URL логируются серверами, прокси, браузерами.
CORS. Настрой список разрешённых доменов явно. Wildcard * допустим только для публичных API без аутентификации.
Валидация входных данных. Никогда не доверяй клиенту. Проверяй типы, длины, форматы на сервере.
Идемпотентность
Идемпотентный запрос — тот, который можно выполнить несколько раз с одним результатом. Это важно при сетевых сбоях: клиент не знает, дошёл ли запрос, и повторяет его.
GET, PUT, DELETE, HEAD — идемпотентны по определению
POST — не идемпотентен (каждый вызов создаёт новый ресурс)
PATCH — зависит от реализации
Для POST-запросов, где важна идемпотентность (например, создание платежа), используй идемпотентный ключ:
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Сервер сохраняет результат по ключу и при повторном запросе возвращает тот же ответ без повторного выполнения операции. Именно так работает Stripe.
Документация — часть продукта
API без документации — это загадка. Даже идеально спроектированный API бесполезен, если разработчик тратит часы на угадывание параметров.
OpenAPI (Swagger) стал индустриальным стандартом. Описывай API в YAML или JSON, и ты получишь:
- Автогенерированную документацию
- Тестовый UI прямо в браузере
- Возможность генерировать SDK для разных языков
- Валидацию контракта
Минимум для каждого эндпоинта:
- Описание, что делает
- Все параметры с типами и примерами
- Возможные ответы (успех и ошибки)
- Пример запроса и ответа
Xороший пример документации — Stripe API Docs. Открой и посмотри, как это должно выглядеть.
Практические советы напоследок
Используй существующие соглашения. Не изобретай формат дат — используй ISO 8601 (2026-04-17T10:30:00Z). Не придумывай структуру ошибок с нуля — посмотри на RFC 7807 (Problem Details for HTTP APIs).
Версионируй с первого дня. Даже если API внутренний. Когда появится второй потребитель (а он появится), ты скажешь себе спасибо.
Не усложняй. REST не требует строгого соответствия всем принципам Fielding. Прагматичный REST лучше академически чистого, но неудобного в использовании.
Думай о клиенте. Перед тем как зафиксировать контракт, попробуй написать клиентский код, который будет использовать твой API. Сразу станет понятно, что неудобно.
Когда в REEXY разрабатывают API в рамках проектов — будь то интеграция сервисов (от 1 500 ₽) или полноценный бэкенд для интернет-магазина — мы закладываем версионирование и единый формат ошибок с первого эндпоинта. Переделывать это потом на живом проекте болезненно и дорого.
Тестируй контракт, а не только код. Используй contract testing (Pact, Dredd) — это гарантирует, что изменения на сервере не сломают существующих клиентов.
Логируй запросы. Уникальный Request-ID в заголовке ответа позволяет быстро найти конкретный запрос в логах. Добавить это несложно, а ценность огромная при дебаггинге.
Хорошо спроектированный API — это инвестиция. Он снижает количество вопросов от разработчиков, упрощает интеграцию, уменьшает количество ошибок. Потраченное время на проектирование возвращается в виде сэкономленных часов поддержки.