Принципы хорошего API
Хороший API -- предсказуемый, консистентный и удобный для разработчиков. Как говорил Джошуа Блох (архитектор Java): "Public APIs, like diamonds, are forever."
Принципы API Design:
┌──────────────────────────────────────────────┐
│ 1. Консистентность -- одинаковые паттерны │
│ 2. Предсказуемость -- ожидаемое поведение │
│ 3. Простота -- минимальная поверхность API │
│ 4. Обратная совместимость -- не ломать │
│ 5. Самодокументируемость -- понятные имена │
└──────────────────────────────────────────────┘
REST Best Practices
Именование ресурсов
✅ Правильно (существительные, множественное число):
GET /api/v1/orders -- список заказов
GET /api/v1/orders/123 -- конкретный заказ
POST /api/v1/orders -- создать заказ
PUT /api/v1/orders/123 -- обновить полностью
PATCH /api/v1/orders/123 -- частичное обновление
DELETE /api/v1/orders/123 -- удалить
✅ Вложенные ресурсы:
GET /api/v1/orders/123/items -- items заказа
POST /api/v1/orders/123/items -- добавить item
GET /api/v1/users/456/orders -- заказы пользователя
❌ Неправильно:
GET /api/v1/getOrders -- глагол в URL
POST /api/v1/order/create -- дублирование метода
GET /api/v1/order -- единственное число
POST /api/v1/orders/123/cancel -- допустимо как действие*
*Действия (actions) допустимы для операций, которые не укладываются в CRUD:
POST /api/v1/orders/123/cancel
POST /api/v1/orders/123/refund
POST /api/v1/payments/123/capture
HTTP статус коды
Успешные (2xx):
200 OK -- GET, PUT, PATCH (с телом)
201 Created -- POST (ресурс создан, Location header)
204 No Content -- DELETE (успех, без тела)
Ошибки клиента (4xx):
400 Bad Request -- невалидные данные
401 Unauthorized -- не аутентифицирован
403 Forbidden -- нет прав
404 Not Found -- ресурс не найден
409 Conflict -- конфликт состояния (дубликат)
422 Unprocessable -- валидация бизнес-правил
429 Too Many Req -- rate limit
Ошибки сервера (5xx):
500 Internal Error -- необработанная ошибка
502 Bad Gateway -- проблема с upstream
503 Unavailable -- сервис перегружен
504 Gateway Timeout -- upstream не ответил
Стандартный формат ошибок (RFC 7807)
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient Funds",
"status": 422,
"detail": "Account balance is $10.00, but order total is $25.50",
"instance": "/api/v1/orders/123/payment",
"errors": [
{
"field": "payment_method",
"message": "Insufficient balance for this payment method"
}
],
"trace_id": "abc-123-def-456"
}
Пагинация
Offset-based (простая, но проблемная)
GET /api/v1/orders?page=5&per_page=20
Ответ:
{
"data": [...],
"meta": {
"page": 5,
"per_page": 20,
"total": 1847,
"total_pages": 93
}
}
Проблема: OFFSET 100000 LIMIT 20 -- медленный запрос
Проблема: Если между запросами добавили запись -- дубликат
Cursor-based (правильный подход)
GET /api/v1/orders?cursor=eyJpZCI6MTIzfQ&limit=20
Ответ:
{
"data": [...],
"meta": {
"next_cursor": "eyJpZCI6MTQzfQ",
"has_more": true
}
}
Cursor = Base64 encoded {"id": 123}
SQL: WHERE id > 123 ORDER BY id ASC LIMIT 20
Преимущества:
+ Стабильная производительность (O(1) vs O(n))
+ Нет пропусков/дубликатов при параллельных записях
+ Подходит для infinite scroll
Недостатки:
- Нельзя перейти на страницу N
- Нет total count (дорогой запрос)
Keyset Pagination для сложных сортировок
GET /api/v1/orders?sort=created_at&after_date=2026-01-15&after_id=456&limit=20
SQL: WHERE (created_at, id) > ('2026-01-15', 456)
ORDER BY created_at ASC, id ASC
LIMIT 20
Версионирование API
Стратегии версионирования
1. URL Path (самый распространённый):
GET /api/v1/orders
GET /api/v2/orders
2. Header:
GET /api/orders
Accept: application/vnd.myapi.v2+json
3. Query Parameter:
GET /api/orders?version=2
4. Content Negotiation:
GET /api/orders
Accept: application/json; version=2
Когда нужна новая версия
Breaking Changes (нужна новая версия):
❌ Удаление поля
❌ Переименование поля
❌ Изменение типа поля
❌ Изменение семантики
❌ Обязательный новый параметр
Non-Breaking Changes (БЕЗ новой версии):
✅ Добавление необязательного поля
✅ Добавление нового endpoint
✅ Добавление необязательного параметра
✅ Расширение enum (с осторожностью)
Стратегия совместимости Stripe
Stripe -- эталон API design. Их подход:
- URL версия (
/v1/) меняется крайне редко - Каждый аккаунт привязан к "API version" (дата, например
2025-12-15) - Новые версии -- автоматическая миграция через transform layer
- Старые версии поддерживаются годами
┌──────────┐ /v1/charges ┌──────────────────┐
│ Client │────────────────>│ API Gateway │
│ v2024 │ Stripe-Version │ │
│ │ :2024-06-20 │ Transform Layer │
└──────────┘ │ 2024 -> 2025 │
│ 2025 -> current │
└────────┬─────────┘
│
v
┌────────────────┐
│ Current Code │
│ (latest ver.) │
└────────────────┘
HATEOAS
Hypermedia as the Engine of Application State -- клиент навигирует по API через ссылки в ответе.
{
"id": "order-123",
"status": "pending_payment",
"total": 2550,
"_links": {
"self": { "href": "/api/v1/orders/order-123" },
"pay": { "href": "/api/v1/orders/order-123/pay", "method": "POST" },
"cancel": { "href": "/api/v1/orders/order-123/cancel", "method": "POST" },
"items": { "href": "/api/v1/orders/order-123/items" }
}
}
Доступные действия зависят от состояния. Если заказ оплачен -- ссылки pay не будет, но появится refund.
На практике: HATEOAS полностью реализуют редко. Чаще используют частичный подход -- ссылки на связанные ресурсы для навигации.
API Gateway
Зачем нужен API Gateway
Без API Gateway:
┌──────────┐──── /users ────> User Service (port 8001)
│ Client │──── /orders ───> Order Service (port 8002)
│ │──── /payments ─> Payment Service (port 8003)
└──────────┘
Клиент знает адреса всех сервисов -- плохо
С API Gateway:
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Client │────>│ API Gateway │────>│ Services │
│ │ │ │ │ │
│ Один │ │ Routing │ │ User │
│ адрес │ │ Auth │ │ Order │
│ │ │ Rate Limit │ │ Payment │
│ │ │ Caching │ │ │
│ │ │ Logging │ │ │
│ │ │ Transform │ │ │
└──────────┘ └──────────────┘ └──────────┘
Функции API Gateway
| Функция | Описание |
|---|---|
| Routing | Направление запросов к нужному сервису |
| Authentication | Проверка JWT/OAuth токенов |
| Rate Limiting | Ограничение запросов per client |
| Caching | Кеширование GET запросов |
| Request/Response Transform | Преобразование форматов |
| Load Balancing | Распределение нагрузки |
| Circuit Breaking | Защита от каскадных сбоев |
| Logging/Tracing | Централизованное логирование |
Популярные решения
┌─────────────────────────────────────────────┐
│ Kong -- Lua/Nginx, plugins │
│ AWS API GW -- Managed, Lambda интегр. │
│ Envoy -- C++, sidecar proxy │
│ Traefik -- Go, авто-конфигурация │
│ APISIX -- Lua/Nginx, высокая произв│
│ KrakenD -- Go, stateless, быстрый │
└─────────────────────────────────────────────┘
BFF Pattern (Backend for Frontend)
Один API Gateway -- не всегда хорошо. Разные клиенты (web, mobile, IoT) имеют разные потребности.
Без BFF:
┌──────┐ ┌──────┐ ┌──────┐
│ Web │ │Mobile│ │ TV │
└──┬───┘ └──┬───┘ └──┬───┘
│ │ │
└─────────┼─────────┘
│
┌────────┴────────┐
│ Generic API │ -- пытается угодить всем
│ Gateway │ -- толстые ответы для mobile
│ │ -- тонкие ответы для web
└─────────────────┘
С BFF:
┌──────┐ ┌──────┐ ┌──────┐
│ Web │ │Mobile│ │ TV │
└──┬───┘ └──┬───┘ └──┬───┘
│ │ │
v v v
┌──────┐ ┌────────┐ ┌─────────┐
│ Web │ │ Mobile │ │ TV │
│ BFF │ │ BFF │ │ BFF │
└──┬───┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼────────────┘
│
┌────────┴────────┐
│ Microservices │
└─────────────────┘
Когда использовать BFF
- Mobile нужны минимальные ответы (трафик, батарея)
- Web нужна агрегация данных для SPA
- TV/IoT нужны совсем другие форматы
- Разные команды разрабатывают разные клиенты
Netflix пример
Netflix имеет отдельные BFF для каждой платформы:
- iOS BFF -- оптимизирован для iPhone/iPad
- Android BFF -- учитывает разнообразие устройств
- TV BFF -- упрощённый UI, большие изображения
- Web BFF -- полнофункциональный
Каждый BFF вызывает одни и те же backend-сервисы, но формирует ответ под свой клиент.
API Documentation
OpenAPI (Swagger)
# openapi.yaml
openapi: 3.1.0
info:
title: Order Service API
version: 1.0.0
paths:
/api/v1/orders:
get:
summary: List orders
parameters:
- name: cursor
in: query
schema:
type: string
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Orders list
content:
application/json:
schema:
$ref: '#/components/schemas/OrderList'
post:
summary: Create order
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Order created
headers:
Location:
schema:
type: string
Правила документации API
- Генерировать из кода -- OpenAPI spec должна генерироваться автоматически
- Примеры -- каждый endpoint имеет реальный пример запроса/ответа
- Описание ошибок -- каждый код ошибки задокументирован
- Changelog -- история изменений API