ИПП Дизайн
"A good API is not just easy to use but also hard to misuse." — Joshua Bloch
API (Application Programming Interface) — это контракт между системами. В распределённой архитектуре, где десятки и сотни сервисов общаются друг с другом, качество API определяет скорость разработки, надёжность интеграций и возможность эволюции системы без ломающих изменений. Плохо спроектированный API — это технический долг, который растёт экспоненциально: каждый новый клиент API усиливает зависимость от неудачных решений.
В этой главе мы разберём лучшие практики проектирования RESTful API, стратегии версионирования, три подхода к пагинации (offset, cursor, keyset), реализацию rate limiting и throttling, принципы идемпотентности операций, а также паттерн API Gateway — центральную точку входа в микросервисную архитектуру.
10.1 RESTful API: лучшие практики
REST (Representational State Transfer) — архитектурный стиль, определённый Роем Филдингом в его диссертации (2000). REST не является протоколом или стандартом — это набор ограничений (constraints), которые при правильном применении дают масштабируемый, предсказуемый и простой в использовании API.
10.1.1 Ресурсы и URL-дизайн
В REST центральное понятие — ресурс. Ресурс — это любая сущность, которую можно идентифицировать: пользователь, заказ, статья, платёж. URL (Uniform Resource Locator) идентифицирует ресурс, а HTTP-метод определяет действие над ним.
Правила именования URL:
Хорошо: Плохо:
GET /users GET /getUsers
GET /users/123 GET /getUserById?id=123
POST /users POST /createUser
PUT /users/123 POST /updateUser
DELETE /users/123 POST /deleteUser?id=123
GET /users/123/orders GET /getOrdersByUser?userId=123
GET /users/123/orders/456 GET /getOrder?userId=123&orderId=456
Ключевые принципы:
-
Существительные, а не глаголы. URL описывает ресурс (
/users), а не действие (/getUsers). Действие определяется HTTP-методом. -
Множественное число.
/users, а не/user. Коллекция ресурсов — всегда во множественном числе, даже когда запрашивается один элемент (/users/123). -
Иерархия через вложенность. Вложенные ресурсы отражают отношения:
/users/123/orders— заказы пользователя 123. Но не стоит углубляться более чем на 2–3 уровня:/users/123/orders/456/items/789/reviews— слишком глубоко, лучше/order-items/789/reviews. -
Kebab-case для составных имён.
/order-items, а не/orderItemsили/order_items. URL нечувствительны к регистру по стандарту, но kebab-case — устоявшаяся конвенция. -
Без расширений файлов.
/users/123, а не/users/123.json. Формат определяется заголовкомAccept.
10.1.2 HTTP-методы и семантика
Каждый HTTP-метод имеет чёткую семантику, определённую RFC 7231:
| Метод | Семантика | Идемпотентный | Безопасный | Тело запроса |
|---|---|---|---|---|
| GET | Получить ресурс | Да | Да | Нет |
| POST | Создать ресурс | Нет | Нет | Да |
| PUT | Заменить ресурс целиком | Да | Нет | Да |
| PATCH | Частично обновить ресурс | Нет* | Нет | Да |
| DELETE | Удалить ресурс | Да | Нет | Нет |
| HEAD | Получить заголовки (без тела) | Да | Да | Нет |
| OPTIONS | Получить допустимые методы | Да | Да | Нет |
*PATCH может быть идемпотентным в зависимости от реализации (JSON Merge Patch — идемпотентен, JSON Patch — нет).
Безопасный метод — не изменяет состояние сервера. GET и HEAD безопасны: кэши, прокси и поисковые роботы могут вызывать их без побочных эффектов.
Идемпотентный метод — повторный вызов с теми же параметрами даёт тот же результат. PUT и DELETE идемпотентны: повторный DELETE /users/123 не вызовет ошибку, ресурс уже удалён.
# PUT: полная замена ресурса (идемпотентно) PUT /users/123 { "name": "Alice Smith", "email": "alice@example.com", "role": "admin" } # Повторный вызов с теми же данными — результат тот же # PATCH: частичное обновление PATCH /users/123 { "role": "admin" } # Обновляется только поле role, остальные не затрагиваются # POST: создание нового ресурса (НЕ идемпотентно) POST /users { "name": "Bob", "email": "bob@example.com" } # Повторный вызов создаст дубликат
10.1.3 HTTP-статус коды
Правильное использование статус-кодов — один из маркеров качественного API. Клиент должен иметь возможность понять результат операции по одному статус-коду, не парся тело ответа.
2xx — успех:
| Код | Имя | Когда использовать |
|---|---|---|
| 200 | OK | GET (данные найдены), PUT/PATCH (ресурс обновлён) |
| 201 | Created | POST (ресурс создан). Заголовок Location указывает URL нового ресурса |
| 202 | Accepted | Запрос принят, но обработка ещё не завершена (асинхронная операция) |
| 204 | No Content | DELETE (ресурс удалён), PUT/PATCH без тела ответа |
4xx — ошибка клиента:
| Код | Имя | Когда использовать |
|---|---|---|
| 400 | Bad Request | Невалидный запрос (ошибка парсинга, невалидные данные) |
| 401 | Unauthorized | Отсутствует или невалидный токен аутентификации |
| 403 | Forbidden | Аутентификация пройдена, но нет прав на операцию |
| 404 | Not Found | Ресурс не найден |
| 405 | Method Not Allowed | Метод не поддерживается для данного ресурса |
| 409 | Conflict | Конфликт (например, ресурс уже существует, конфликт версий) |
| 422 | Unprocessable Entity | Синтаксически верный запрос, но семантически некорректный (бизнес-валидация) |
| 429 | Too Many Requests | Rate limit превышен |
5xx — ошибка сервера:
| Код | Имя | Когда использовать |
|---|---|---|
| 500 | Internal Server Error | Необработанная ошибка на сервере |
| 502 | Bad Gateway | Прокси/LB не смог получить ответ от upstream |
| 503 | Service Unavailable | Сервис временно недоступен (перегрузка, обслуживание) |
| 504 | Gateway Timeout | Прокси/LB: таймаут ожидания ответа от upstream |
Антипаттерн: всегда 200 с ошибкой в теле:
// ПЛОХО: HTTP 200, ошибка спрятана в теле { "success": false, "error": "User not found" } // ХОРОШО: HTTP 404 с описанием ошибки // Status: 404 Not Found { "error": { "code": "USER_NOT_FOUND", "message": "User with id 123 not found", "details": [] } }
10.1.4 Формат ошибок
Стандартизированный формат ошибок экономит часы отладки. RFC 7807 (Problem Details for HTTP APIs) определяет формат:
{ "type": "https://api.example.com/errors/insufficient-funds", "title": "Insufficient funds", "status": 422, "detail": "Account balance is $30.00, but transaction requires$50.00", "instance": "/transactions/txn-789", "balance": 30.00, "required": 50.00 }
На практике многие API используют упрощённый формат:
{ "error": { "code": "INSUFFICIENT_FUNDS", "message": "Account balance is $30.00, but transaction requires$50.00", "details": [ { "field": "amount", "reason": "exceeds_balance", "message": "Amount $50.00 exceeds available balance$30.00" } ], "request_id": "req-abc-123" } }
request_id — критически важное поле. Оно позволяет клиенту сообщить в support «запрос req-abc-123 вернул ошибку», а инженеру — найти этот запрос в логах и трейсах.
10.1.5 Фильтрация, сортировка и выбор полей
Фильтрация:
GET /orders?status=pending&created_after=2025-01-01&min_amount=100
Параметры фильтрации — query parameters. Не стоит помещать фильтры в тело GET-запроса (технически возможно, но противоречит семантике GET и ломает кэширование).
Сортировка:
GET /users?sort=created_at # по возрастанию
GET /users?sort=-created_at # по убыванию (префикс -)
GET /users?sort=role,-created_at # сначала по роли, потом по дате (убыв.)
Выбор полей (partial response):
GET /users/123?fields=id,name,email
Позволяет клиенту запросить только нужные поля — снижает трафик и нагрузку на сериализацию. Google API, Facebook Graph API и Stripe активно используют этот подход.
// GET /users/123 { "id": 123, "name": "Alice", "email": "alice@example.com", "bio": "...(длинный текст)...", "avatar_url": "...", "created_at": "2025-01-01T00:00:00Z", "settings": { ... } } // GET /users/123?fields=id,name,email { "id": 123, "name": "Alice", "email": "alice@example.com" }
10.1.6 HATEOAS и зрелость API
Модель зрелости Ричардсона описывает четыре уровня REST API:
| Уровень | Описание | Пример |
|---|---|---|
| 0 | Один URL, один метод (POST) | SOAP, XML-RPC |
| 1 | Отдельные URL для ресурсов | /users, /orders |
| 2 | Использование HTTP-методов и статус-кодов | GET, POST, PUT, DELETE + 2xx/4xx |
| 3 | HATEOAS — ссылки в ответах | Ссылки на связанные ресурсы и действия |
HATEOAS (Hypermedia As The Engine Of Application State):
// GET /orders/456 { "id": 456, "status": "pending", "amount": 99.99, "_links": { "self": { "href": "/orders/456" }, "cancel": { "href": "/orders/456/cancel", "method": "POST" }, "payment": { "href": "/orders/456/payment", "method": "POST" }, "user": { "href": "/users/123" } } }
Клиент не хардкодит URL-ы — он получает доступные действия и ссылки из ответа. На практике полноценный HATEOAS используется редко (GitHub API — один из немногих примеров), но включение ссылки self и связанных ресурсов — хорошая практика.
Большинство production API находятся на уровне 2, и это нормально. Уровень 2 — прагматичный выбор для большинства систем.
10.2 Версионирование API
API эволюционирует. Добавляются новые поля, меняются форматы, удаляются устаревшие эндпоинты. Если у API есть внешние потребители, нельзя просто изменить контракт — это сломает все клиентские интеграции. Версионирование — механизм, позволяющий вносить ломающие изменения (breaking changes) без нарушения работы существующих клиентов.
10.2.1 Что является breaking change
Breaking changes (нарушают контракт):
- Удаление поля из ответа
- Удаление или переименование эндпоинта
- Изменение типа поля (
"price": "99.99"→"price": 99.99) - Добавление обязательного параметра в запросе
- Изменение семантики существующего поля
- Изменение кодов ошибок
Non-breaking changes (безопасны):
- Добавление нового необязательного поля в ответ
- Добавление нового эндпоинта
- Добавление необязательного параметра в запрос
- Добавление нового статус-кода (при условии, что клиент обрабатывает неизвестные коды по классу: 4xx = ошибка клиента)
Принцип Postеl (Robustness Principle): «Будь консервативен в том, что отправляешь, и либерален в том, что принимаешь». Клиент API должен игнорировать неизвестные поля, а сервер — принимать запросы с отсутствующими необязательными полями.
10.2.2 Стратегии версионирования
1. Версия в URL (URI versioning):
GET /v1/users/123
GET /v2/users/123
Самый простой и распространённый подход. Используется в Stripe, Twitter, Google Maps, GitHub.
Преимущества:
- Очевидно и наглядно
- Легко тестировать (можно открыть в браузере)
- Легко маршрутизировать (Nginx, API Gateway)
- Легко кэшировать (разные URL = разные кэш-ключи)
Недостатки:
- Нарушает принцип REST (URL идентифицирует ресурс, а не его версию)
- При изменении одного эндпоинта нужно поддерживать весь
/v1/и весь/v2/ - Клиент должен обновить URL везде
2. Версия в заголовке (Header versioning):
GET /users/123
Accept: application/vnd.example.v2+json
Или кастомный заголовок:
GET /users/123
X-API-Version: 2
Используется в GitHub API (application/vnd.github.v3+json).
Преимущества:
- URL остаётся чистым — один URL для всех версий
- Соответствует принципам REST
- Версионируются отдельные ресурсы, а не весь API
Недостатки:
- Не видно в URL (сложнее тестировать, делиться ссылками)
- Сложнее маршрутизировать на уровне прокси
- Клиент должен явно устанавливать заголовок
3. Версия в query parameter:
GET /users/123?version=2
Используется в Amazon AWS (некоторые API).
Преимущества:
- Простота — не требует изменения URL-пути
- Опциональность — если не указана, используется версия по умолчанию
Недостатки:
- Легко забыть указать — неявное поведение
- Замусоривает query string
4. Версионирование через Content Negotiation:
GET /users/123
Accept: application/json; version=2
Близко к header versioning, но использует стандартный механизм HTTP.
10.2.3 Рекомендации по версионированию
Практическая рекомендация: Для большинства API — версия в URL (/v1/). Это самый простой, понятный и широко поддерживаемый подход. Stripe, один из лучших примеров API-дизайна, использует именно этот подход.
Стратегия Stripe: версионирование по дате:
Stripe-Version: 2025-01-15
Stripe версионирует изменения по дате. При регистрации аккаунт «закрепляется» за текущей версией API. Разработчик может явно указать версию в заголовке. Это позволяет вносить изменения инкрементально, не создавая монолитных /v2/.
Как долго поддерживать старые версии:
- Публичные API (SaaS, платёжные системы): минимум 12–24 месяца после выпуска новой версии. Stripe поддерживает все версии с 2011 года.
- Внутренние API (между микросервисами): 1–3 месяца. Вы контролируете все клиенты и можете мигрировать быстро.
- Партнёрские API (B2B интеграции): 6–12 месяцев с уведомлением партнёров.
Deprecation policy:
HTTP/1.1 200 OK Deprecation: Sun, 01 Jun 2025 00:00:00 GMT Sunset: Mon, 01 Dec 2025 00:00:00 GMT Link: <https://api.example.com/v3/users>; rel="successor-version"
Заголовок Deprecation (RFC 8594) указывает, что эндпоинт устарел. Sunset — дату отключения. Link — ссылку на новую версию.
10.3 Пагинация
Любой эндпоинт, возвращающий коллекцию, должен поддерживать пагинацию. Без неё запрос GET /orders для пользователя с миллионом заказов вернёт гигабайтный ответ, убьёт БД и заставит клиента ждать минуты. Пагинация решает это, возвращая данные порциями.
Существуют три основных подхода к пагинации, каждый со своими trade-offs.
10.3.1 Offset-based пагинация
Клиент указывает смещение (offset) и количество записей (limit):
GET /orders?offset=0&limit=20 → записи 1–20
GET /orders?offset=20&limit=20 → записи 21–40
GET /orders?offset=40&limit=20 → записи 41–60
Или эквивалентная форма page + per_page:
GET /orders?page=1&per_page=20 → записи 1–20
GET /orders?page=2&per_page=20 → записи 21–40
SQL-реализация:
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 40;
Формат ответа:
{ "data": [ { "id": 41, "amount": 99.99 }, { "id": 42, "amount": 150.00 } ], "pagination": { "total": 10543, "offset": 40, "limit": 20, "has_more": true } }
Преимущества:
- Простота реализации — одна строка SQL
- Возможность «прыгать» на произвольную страницу (страница 50 из 100)
- Клиент знает общее количество записей (
total) и может показать номера страниц
Недостатки:
- Производительность деградирует при больших offset.
OFFSET 1000000заставляет БД прочитать и отбросить 1 миллион строк, а потом вернуть 20. Время растёт линейно с offset.
OFFSET 0: ~2 ms
OFFSET 10000: ~50 ms
OFFSET 100000: ~500 ms
OFFSET 1000000: ~5000 ms ← неприемлемо
- Нестабильность при изменении данных. Если между запросами страниц 1 и 2 в начало списка добавилась новая запись — пользователь увидит одну запись дважды (она «сдвинулась» на следующую страницу). Если запись удалена — одна запись будет пропущена.
Страница 1: [A, B, C, D, E] (offset=0)
↓ Между запросами добавлена запись X в начало
Страница 2: [E, F, G, H, I] (offset=5)
→ Запись E видна и на странице 1, и на странице 2 (дубликат)
- COUNT(*) для
total— дорого. Подсчёт общего числа записей может быть тяжёлой операцией для больших таблиц. Решение: не возвращатьtotalили кэшировать его с TTL.
Когда использовать: Простые UI с номерами страниц, административные панели, внутренние API с небольшими датасетами (< 100K записей).
10.3.2 Cursor-based пагинация
Вместо числового смещения клиент получает непрозрачный курсор (обычно закодированный идентификатор последнего элемента) и передаёт его для получения следующей порции.
GET /orders?limit=20 → записи 1–20
→ Response: { "data": [...], "next_cursor": "eyJpZCI6MjB9" }
GET /orders?limit=20&cursor=eyJpZCI6MjB9 → записи 21–40
→ Response: { "data": [...], "next_cursor": "eyJpZCI6NDB9" }
GET /orders?limit=20&cursor=eyJpZCI6NDB9 → записи 41–60
Курсор — это base64-закодированные параметры, необходимые для определения точки продолжения. Например, eyJpZCI6MjB9 = {"id": 20}.
SQL-реализация:
-- Первая страница SELECT * FROM orders ORDER BY created_at DESC, id DESC LIMIT 20; -- Следующая страница (курсор = id последнего элемента предыдущей страницы) SELECT * FROM orders WHERE (created_at, id) < ('2025-01-15T10:30:00Z', 456) ORDER BY created_at DESC, id DESC LIMIT 20;
Ключевое отличие: вместо OFFSET N используется WHERE id < cursor_id. БД использует индекс для поиска нужной позиции — это O(log N) вместо O(N).
Реализация на Python:
import base64 import json def encode_cursor(last_item: dict) -> str: payload = { "id": last_item["id"], "created_at": last_item["created_at"].isoformat(), } return base64.urlsafe_b64encode(json.dumps(payload).encode()).decode() def decode_cursor(cursor: str) -> dict: return json.loads(base64.urlsafe_b64decode(cursor.encode()).decode()) def get_orders(cursor: str | None, limit: int = 20) -> dict: query = "SELECT * FROM orders" params = [] if cursor: decoded = decode_cursor(cursor) query += " WHERE (created_at, id) < (%s, %s)" params.extend([decoded["created_at"], decoded["id"]]) query += " ORDER BY created_at DESC, id DESC LIMIT %s" params.append(limit + 1) # Запрашиваем +1 для определения has_more rows = db.execute(query, params) has_more = len(rows) > limit data = rows[:limit] return { "data": data, "pagination": { "next_cursor": encode_cursor(data[-1]) if has_more else None, "has_more": has_more, } }
Формат ответа:
{ "data": [ { "id": 41, "amount": 99.99, "created_at": "2025-01-15T10:00:00Z" }, { "id": 42, "amount": 150.00, "created_at": "2025-01-15T09:55:00Z" } ], "pagination": { "next_cursor": "eyJpZCI6NDIsImNyZWF0ZWRfYXQiOiIyMDI1LTAxLTE1VDA5OjU1OjAwWiJ9", "has_more": true } }
Преимущества:
- Стабильная производительность — O(log N) вне зависимости от глубины пагинации
- Стабильность при изменении данных — нет дубликатов или пропусков при вставке/удалении
- Нет дорогого COUNT(*) —
has_moreопределяется запросом +1 элемента
Недостатки:
- Нельзя «прыгнуть» на произвольную страницу (только последовательный переход)
- Нельзя показать общее количество записей или номер текущей страницы
- Сложнее реализовать — нужна правильная сортировка и составной курсор
Когда использовать: Бесконечная прокрутка (infinite scroll), ленты (feed), мобильные приложения, API с большими датасетами. Используется в Twitter, Facebook, Slack, Stripe.
10.3.3 Keyset-based пагинация
Keyset-пагинация — это, по сути, cursor-пагинация с открытыми (не закодированными) параметрами курсора. Клиент явно передаёт значения полей, по которым происходит навигация.
GET /orders?limit=20
GET /orders?limit=20&created_before=2025-01-15T10:00:00Z&id_before=456
SQL-реализация:
SELECT * FROM orders WHERE (created_at, id) < ('2025-01-15T10:00:00Z', 456) ORDER BY created_at DESC, id DESC LIMIT 20;
Механика та же, что у cursor-пагинации, но курсор «открыт» — клиент видит и контролирует параметры навигации.
Когда использовать: Внутренние API, где клиент знает структуру данных. Для публичных API предпочтительнее cursor (клиент не зависит от внутренней структуры).
10.3.4 Сравнение подходов
| Критерий | Offset | Cursor | Keyset |
|---|---|---|---|
| Произвольный доступ к странице | Да | Нет | Нет |
| Общее количество (total) | Да (дорого) | Нет | Нет |
| Стабильность при изменениях | Нет | Да | Да |
| Производительность на больших offset | Деградирует | Стабильная | Стабильная |
| Простота реализации | Простая | Средняя | Средняя |
| Простота использования клиентом | Простая | Средняя | Сложнее |
Рекомендация: Cursor-based — лучший выбор по умолчанию для новых API. Offset допустим для административных интерфейсов с предсказуемо малыми датасетами.
10.4 Rate Limiting и Throttling
Rate limiting — механизм ограничения количества запросов к API за единицу времени. Без rate limiting один клиент (злонамеренный или с багом) может «положить» весь сервис, отправляя миллионы запросов.
10.4.1 Зачем нужен Rate Limiting
- Защита от злоупотреблений. Боты, скрейперы, DDoS-атаки, brute-force паролей.
- Справедливое распределение ресурсов. Один клиент не должен потреблять все ресурсы, лишая остальных обслуживания.
- Предотвращение каскадных отказов. Шквал запросов от одного сервиса может перегрузить downstream-зависимости.
- Контроль затрат. API-провайдеры ограничивают бесплатные тарифы и тарифицируют избыточное использование.
10.4.2 Алгоритмы Rate Limiting
Token Bucket:
Представьте ведро, в которое периодически добавляются токены. Каждый запрос «потребляет» один токен. Если ведро пустое — запрос отклоняется. Ведро имеет максимальную ёмкость — это позволяет обрабатывать короткие всплески трафика.
Token Bucket:
Capacity: 10 токенов
Refill rate: 2 токена/секунду
Время 0: [●●●●●●●●●●] 10 токенов (полное ведро)
Запрос: [●●●●●●●●●○] 9 токенов
Запрос: [●●●●●●●●○○] 8 токенов
...
10 запросов подряд:
[○○○○○○○○○○] 0 токенов → следующий запрос отклонён (429)
Через 1 сек: [●●○○○○○○○○] 2 токена (пополнение)
import time from threading import Lock class TokenBucket: def __init__(self, capacity: int, refill_rate: float): self.capacity = capacity self.refill_rate = refill_rate # токенов в секунду self.tokens = capacity self.last_refill = time.monotonic() self.lock = Lock() def allow(self) -> bool: with self.lock: now = time.monotonic() elapsed = now - self.last_refill self.tokens = min( self.capacity, self.tokens + elapsed * self.refill_rate ) self.last_refill = now if self.tokens >= 1: self.tokens -= 1 return True return False
Преимущества: Позволяет короткие всплески (burst), простота, широкое использование (AWS API Gateway, Stripe, GitHub).
Leaky Bucket:
Запросы попадают в «дырявое ведро» и обрабатываются с фиксированной скоростью. Если ведро переполняется — запросы отклоняются. В отличие от Token Bucket, выходная скорость строго постоянна.
Leaky Bucket:
Capacity: 10 запросов в очереди
Drain rate: 2 запроса/секунду
Входящие: [req1, req2, req3, ...] → в ведро
Исходящие: [req1] ... [req2] ... [req3] → 1 запрос каждые 500ms
Если ведро полное → 429 Too Many Requests
Преимущества: Гарантирует постоянную скорость обработки — хорошо для защиты downstream-сервисов. Недостатки: Не позволяет burst — даже легитимные всплески сглаживаются.
Fixed Window Counter:
Время делится на фиксированные окна (например, по 1 минуте). Для каждого окна ведётся счётчик запросов. При превышении лимита в текущем окне запросы отклоняются.
Fixed Window (лимит: 100 запросов/минуту):
|--- 12:00-12:01 ---|--- 12:01-12:02 ---|--- 12:02-12:03 ---|
| 85 запросов | 100 запросов | 42 запроса |
| ✓ | ✓ (лимит) | ✓ |
Проблема: на границе окон
|--- 12:00:30-12:01:00 ---|--- 12:01:00-12:01:30 ---|
| 90 запросов | 90 запросов |
→ 180 запросов за 1 минуту, хотя лимит — 100!
Проблема: На границе окон клиент может отправить до 2× лимита (90 в конце одного окна + 90 в начале следующего = 180 за минуту при лимите 100).
Sliding Window Log:
Хранит timestamp каждого запроса. При проверке лимита подсчитывает запросы в скользящем окне (последние N секунд).
class SlidingWindowLog: def __init__(self, limit: int, window_seconds: int): self.limit = limit self.window = window_seconds self.requests: dict[str, list[float]] = {} # client_id → [timestamps] def allow(self, client_id: str) -> bool: now = time.time() cutoff = now - self.window if client_id not in self.requests: self.requests[client_id] = [] # Удаляем устаревшие записи self.requests[client_id] = [ ts for ts in self.requests[client_id] if ts > cutoff ] if len(self.requests[client_id]) < self.limit: self.requests[client_id].append(now) return True return False
Преимущества: Точный подсчёт, нет проблемы границ окон. Недостатки: Высокое потребление памяти — хранение всех timestamps. Для 1M клиентов × 1000 запросов/минуту = 1 миллиард записей.
Sliding Window Counter:
Гибрид Fixed Window и Sliding Window Log. Использует счётчики двух соседних фиксированных окон и взвешивает их пропорционально текущему положению в окне.
Текущее время: 12:01:15 (15 секунд в текущем окне)
Размер окна: 60 секунд
Предыдущее окно (12:00:00-12:01:00): 80 запросов
Текущее окно (12:01:00-12:02:00): 30 запросов
Вес предыдущего окна: (60 - 15) / 60 = 0.75
Оценка: 80 × 0.75 + 30 = 90 запросов
Если лимит 100 → запрос разрешён (90 < 100)
class SlidingWindowCounter: def __init__(self, limit: int, window_seconds: int): self.limit = limit self.window = window_seconds def allow(self, client_id: str) -> bool: now = time.time() current_window = int(now // self.window) position_in_window = now % self.window # Получаем счётчики из Redis current_count = int(redis.get(f"rl:{client_id}:{current_window}") or 0) prev_count = int(redis.get(f"rl:{client_id}:{current_window - 1}") or 0) # Взвешенная оценка weight = (self.window - position_in_window) / self.window estimated = prev_count * weight + current_count if estimated < self.limit: pipe = redis.pipeline() pipe.incr(f"rl:{client_id}:{current_window}") pipe.expire(f"rl:{client_id}:{current_window}", self.window * 2) pipe.execute() return True return False
Преимущества: Точность, близкая к Sliding Window Log, при потреблении памяти Fixed Window (два счётчика на клиента). Это оптимальный алгоритм для большинства production-систем.
10.4.3 Сравнение алгоритмов
| Алгоритм | Точность | Память | Burst | Сложность |
|---|---|---|---|---|
| Token Bucket | Высокая | O(1) на клиента | Да | Простая |
| Leaky Bucket | Высокая | O(очередь) | Нет | Средняя |
| Fixed Window | Низкая (граница окон) | O(1) на клиента | Да (до 2×) | Простая |
| Sliding Window Log | Точная | O(N запросов) | Да | Средняя |
| Sliding Window Counter | Высокая (~99.7%) | O(1) на клиента | Да | Средняя |
10.4.4 Распределённый Rate Limiting
В системе с несколькими инстансами приложения rate limiter должен быть общим — иначе каждый инстанс считает независимо, и клиент может отправить limit × N_instances запросов.
Централизованный подход (Redis):
def rate_limit_redis(client_id: str, limit: int, window: int) -> bool: key = f"rate_limit:{client_id}:{int(time.time()) // window}" pipe = redis.pipeline() pipe.incr(key) pipe.expire(key, window) result = pipe.execute() count = result[0] return count <= limit
Redis — стандартное решение для распределённого rate limiting. Одна операция INCR + EXPIRE атомарна через pipeline. Латенси ~1ms.
Lua-скрипт для Token Bucket в Redis:
-- KEYS[1] = rate limit key -- ARGV[1] = capacity -- ARGV[2] = refill_rate (tokens/sec) -- ARGV[3] = current timestamp local key = KEYS[1] local capacity = tonumber(ARGV[1]) local refill_rate = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local data = redis.call('HMGET', key, 'tokens', 'last_refill') local tokens = tonumber(data[1]) or capacity local last_refill = tonumber(data[2]) or now -- Пополняем токены local elapsed = now - last_refill tokens = math.min(capacity, tokens + elapsed * refill_rate) if tokens >= 1 then tokens = tokens - 1 redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now) redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) * 2) return 1 -- Разрешён else redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now) return 0 -- Отклонён end
Lua-скрипт выполняется атомарно на сервере Redis — нет race condition между чтением и записью.
10.4.5 HTTP-заголовки Rate Limiting
RFC 6585 определяет статус 429, а draft RFC «RateLimit Fields for HTTP» стандартизирует заголовки:
HTTP/1.1 200 OK RateLimit-Limit: 100 RateLimit-Remaining: 42 RateLimit-Reset: 1705312800 # При превышении лимита: HTTP/1.1 429 Too Many Requests RateLimit-Limit: 100 RateLimit-Remaining: 0 RateLimit-Reset: 1705312800 Retry-After: 30
| Заголовок | Описание |
|---|---|
RateLimit-Limit | Максимальное количество запросов в окне |
RateLimit-Remaining | Оставшееся количество запросов |
RateLimit-Reset | Unix timestamp сброса окна |
Retry-After | Секунды до возможности повторить запрос |
Практическая рекомендация: Всегда возвращайте эти заголовки — даже в успешных ответах. Клиент может адаптировать скорость запросов, не доводя до 429.
10.4.6 Многоуровневый Rate Limiting
В production rate limiting обычно применяется на нескольких уровнях:
Клиент → CDN/WAF (Cloudflare) → API Gateway → Сервис
│ │ │
└─ IP-based └─ API key/ └── Per-user,
DDoS protection token-based per-endpoint
(10K req/s per IP) (100 req/min) (5 req/s для
тяжёлых операций)
| Уровень | Гранулярность | Лимит | Цель |
|---|---|---|---|
| CDN/WAF | Per IP | Тысячи/с | Защита от DDoS |
| API Gateway | Per API key | Сотни/мин | Коммерческие лимиты, fair use |
| Сервис | Per user, per endpoint | Единицы/с | Защита тяжёлых эндпоинтов |
Различные лимиты для разных операций:
GET /users: 1000 req/min (дешёвая операция)
POST /users: 10 req/min (создание ресурса)
POST /reports: 2 req/min (тяжёлый запрос, генерация отчёта)
POST /login: 5 req/min (защита от brute-force)
10.4.7 Throttling vs Rate Limiting
| Механизм | Поведение | Цель |
|---|---|---|
| Rate Limiting | Отклоняет запросы при превышении лимита (429) | Защита сервиса |
| Throttling | Замедляет обработку запросов (задержка) | Плавная деградация |
Throttling может замедлять ответы вместо отклонения: при приближении к лимиту ответ задерживается на 100ms, 500ms, 2s. Клиент получает ответ, но медленнее — это мягче, чем жёсткий отказ.
10.5 Идемпотентность операций
Идемпотентность — свойство операции, при котором многократное выполнение с одними и теми же параметрами даёт тот же результат, что и однократное. В распределённых системах идемпотентность критична: сетевые сбои, таймауты и retry-механизмы приводят к тому, что один и тот же запрос может быть отправлен несколько раз.
10.5.1 Проблема без идемпотентности
Клиент → POST /payments {"amount": 100, "to": "user-456"}
│
│ Сеть: запрос дошёл до сервера
│ Сервер: обработал, списал $100
│ Сервер → Ответ 200
│
✗ Ответ потерян в сети (таймаут у клиента)
│
│ Клиент: "Не получил ответ, повторю запрос"
│
Клиент → POST /payments {"amount": 100, "to": "user-456"}
│
│ Сервер: обработал ПОВТОРНО, списал ещё $100
│
→ Итого списано $200 вместо$100 💥
Без идемпотентности повторный запрос создаёт дубликат. Для платежей, заказов, переводов — это катастрофа.
10.5.2 Idempotency Key
Стандартное решение — idempotency key. Клиент генерирует уникальный ключ для каждой операции и передаёт его в заголовке. Сервер запоминает результат и при повторном запросе с тем же ключом возвращает сохранённый ответ.
Первый запрос:
POST /payments
Idempotency-Key: "idk-abc-123"
{"amount": 100, "to": "user-456"}
→ Сервер: ключ "idk-abc-123" не найден → обрабатываем
→ Сохраняем: {key: "idk-abc-123", response: 200, body: {...}}
→ Ответ: 200 OK
Повторный запрос (retry):
POST /payments
Idempotency-Key: "idk-abc-123"
{"amount": 100, "to": "user-456"}
→ Сервер: ключ "idk-abc-123" НАЙДЕН → возвращаем сохранённый ответ
→ Ответ: 200 OK (тот же, что и при первом запросе)
→ Повторного списания НЕ произошло ✓
Реализация:
def process_payment(request, idempotency_key: str): # 1. Проверяем, был ли запрос уже обработан existing = db.query( "SELECT response_status, response_body FROM idempotency_keys " "WHERE key = %s AND created_at > NOW() - INTERVAL '24 hours'", idempotency_key ) if existing: # Возвращаем сохранённый ответ return Response( status=existing.response_status, body=existing.response_body ) # 2. Обрабатываем запрос try: result = execute_payment(request) response = Response(status=200, body=result) except InsufficientFunds as e: response = Response(status=422, body={"error": str(e)}) # 3. Сохраняем результат db.execute( "INSERT INTO idempotency_keys (key, response_status, response_body) " "VALUES (%s, %s, %s)", idempotency_key, response.status, response.body ) return response
Race condition: Два параллельных запроса с одним ключом могут оба пройти проверку existing и начать обработку. Решение — INSERT ... ON CONFLICT DO NOTHING или distributed lock:
def process_payment_safe(request, idempotency_key: str): # Атомарная попытка «занять» ключ inserted = db.execute( "INSERT INTO idempotency_keys (key, status) " "VALUES (%s, 'processing') " "ON CONFLICT (key) DO NOTHING " "RETURNING key", idempotency_key ) if not inserted: # Ключ уже существует — ждём результат или возвращаем его return wait_for_result(idempotency_key) try: result = execute_payment(request) db.execute( "UPDATE idempotency_keys " "SET status = 'completed', response_body = %s " "WHERE key = %s", json.dumps(result), idempotency_key ) return Response(status=200, body=result) except Exception as e: db.execute( "UPDATE idempotency_keys " "SET status = 'failed', response_body = %s " "WHERE key = %s", json.dumps({"error": str(e)}), idempotency_key ) raise
10.5.3 Stripe и идемпотентность
Stripe — эталонный пример реализации идемпотентности. Каждый мутирующий запрос принимает заголовок Idempotency-Key:
import stripe stripe.PaymentIntent.create( amount=1000, currency="usd", idempotency_key="pi_unique_key_123", ) # Повторный вызов с тем же ключом вернёт тот же результат
Ключевые решения Stripe:
- Idempotency key хранится 24 часа — после этого ключ можно переиспользовать
- Если тело запроса отличается при том же ключе — ошибка 400 (защита от неправильного использования)
- Если исходный запрос ещё обрабатывается — 409 Conflict (предотвращение race condition)
- Ключ генерирует клиент (UUID v4) — сервер не знает, повторный это запрос или нет, до проверки хранилища
10.5.4 Естественная идемпотентность
Некоторые операции идемпотентны по своей природе:
| Операция | Идемпотентна? | Почему |
|---|---|---|
GET /users/123 | Да | Чтение не меняет состояние |
PUT /users/123 {name: "Alice"} | Да | Повторная замена даёт тот же результат |
DELETE /users/123 | Да | Повторное удаление: ресурс уже удалён |
POST /users {name: "Alice"} | Нет | Повторный вызов создаст дубликат |
POST /payments {amount: 100} | Нет | Повторный вызов спишет дважды |
PATCH /users/123 {balance += 100} | Нет | Инкрементальное изменение |
Превращение неидемпотентных операций в идемпотентные:
# Плохо: POST /balance/add {"amount": 100} # → повторный вызов добавит ещё 100 # Хорошо: PUT /balance/transactions/txn-123 {"amount": 100} # → повторный вызов с тем же txn-123: транзакция уже существует → пропуск
Вместо «добавить 100» — «создать транзакцию с ID=txn-123 на 100». Если транзакция с таким ID уже существует — операция no-op.
10.6 API Gateway
API Gateway — единая точка входа для всех клиентских запросов к микросервисной системе. Вместо того чтобы каждый клиент знал адреса десятков микросервисов и общался с каждым напрямую, клиент обращается к одному API Gateway, который маршрутизирует запросы, применяет cross-cutting concerns и агрегирует ответы.
10.6.1 Зачем нужен API Gateway
Без API Gateway каждый клиент должен:
- Знать адреса всех сервисов (service discovery)
- Реализовать аутентификацию для каждого сервиса
- Обрабатывать rate limiting, retry, circuit breaker
- Агрегировать данные из нескольких сервисов
Без API Gateway: С API Gateway:
Client ──► User Service Client ──► API Gateway
Client ──► Order Service │
Client ──► Payment Service ┌────────────┼────────────┐
Client ──► Notification Service ▼ ▼ ▼
User Svc Order Svc Payment Svc
Клиент знает 4 адреса,
4 схемы аутентификации, Клиент знает 1 адрес,
4 формата ошибок 1 формат аутентификации
10.6.2 Функции API Gateway
┌─────────────────────────────────────┐
│ API Gateway │
│ │
Client ──HTTPS──► │ 1. TLS Termination │
│ 2. Authentication / Authorization │
│ 3. Rate Limiting │
│ 4. Request Validation │
│ 5. Request/Response Transformation │
│ 6. Routing │
│ 7. Load Balancing │
│ 8. Caching │
│ 9. Logging / Tracing │
│ 10. Response Aggregation │
└──────────────────┬──────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
User Svc Order Svc Payment Svc
1. Authentication и Authorization:
Gateway проверяет JWT-токен или API-ключ один раз и передаёт сервисам уже валидированную информацию о пользователе (в заголовках):
Client → Gateway: Authorization: Bearer eyJ...
Gateway: Валидация JWT → извлекает user_id=123, role=admin
Gateway → Service: X-User-Id: 123, X-User-Role: admin
Сервисы доверяют Gateway и не проверяют токен повторно. Это снимает нагрузку с каждого сервиса и централизует логику аутентификации.
2. Request Routing:
# Kong / Nginx-based Gateway location /api/users { proxy_pass http://user-service:8080; } location /api/orders { proxy_pass http://order-service:8080; } location /api/payments { proxy_pass http://payment-service:8080; }
3. Response Aggregation (API Composition):
Один запрос клиента может требовать данных из нескольких сервисов:
Client: GET /api/dashboard
Gateway:
┌──► User Service: GET /users/123 → {name, avatar}
├──► Order Service: GET /users/123/stats → {total_orders, last_order}
└──► Payment Service: GET /users/123/balance → {balance, currency}
Gateway собирает ответы и возвращает:
{
"user": {"name": "Alice", "avatar": "..."},
"orders": {"total": 42, "last_order": "..."},
"balance": {"amount": 500.00, "currency": "USD"}
}
Это паттерн Backend for Frontend (BFF) — Gateway формирует ответ, оптимизированный под конкретный клиент (мобильное приложение, веб, IoT).
4. Request Transformation:
Gateway может трансформировать запросы и ответы: добавлять/удалять заголовки, менять формат (XML → JSON), обогащать данными из других источников.
10.6.3 Технологии API Gateway
| Технология | Тип | Особенности |
|---|---|---|
| Kong | Self-hosted (Nginx + Lua) | Плагинная архитектура, Kubernetes-native, open-source |
| AWS API Gateway | Managed | REST, HTTP, WebSocket API; интеграция с Lambda, IAM |
| Apigee (Google) | Managed | Enterprise-фокус, аналитика, developer portal |
| Tyk | Self-hosted | Go, open-source, GraphQL-поддержка |
| Envoy + Istio | Service Mesh | L7 прокси, динамическая конфигурация, mTLS |
| NGINX Plus | Self-hosted | Высокая производительность, API management |
| Traefik | Self-hosted | Auto-discovery (Docker, K8s), Let's Encrypt |
Kong — наиболее популярный open-source API Gateway:
# Kong declarative config _format_version: "3.0" services: - name: user-service url: http://user-service:8080 routes: - name: user-route paths: - /api/users strip_path: true plugins: - name: rate-limiting config: minute: 100 policy: redis redis_host: redis - name: jwt config: claims_to_verify: - exp - name: correlation-id config: header_name: X-Request-Id generator: uuid - name: prometheus
Kong использует плагины для добавления функциональности: authentication, rate limiting, logging, transformations, CORS — всё подключается декларативно.
10.6.4 Паттерны API Gateway
Backend for Frontend (BFF):
Отдельный Gateway для каждого типа клиента: мобильное приложение получает компактный JSON, веб-приложение — полный, IoT — минимальный.
Mobile App ──► Mobile BFF Gateway ──► Микросервисы
Web App ──► Web BFF Gateway ──► Микросервисы
IoT Device ──► IoT BFF Gateway ──► Микросервисы
Преимущества BFF:
- Каждый Gateway оптимизирован под свой клиент
- Разные команды могут развивать свой BFF независимо
- Мобильный BFF агрегирует данные для одного экрана (один запрос вместо пяти)
Недостатки:
- Дублирование логики между BFF
- Больше сервисов для поддержки
Netflix, SoundCloud и Airbnb используют паттерн BFF.
Edge Functions / Serverless Gateway:
API Gateway на edge (CDN), выполняющий логику ближе к пользователю:
Пользователь → CDN Edge (Cloudflare Workers / Lambda@Edge)
│
├── Аутентификация (проверка JWT на edge)
├── A/B testing (выбор варианта)
├── Кэширование (ответ из кэша CDN)
└── Routing → Origin (если не кэшировано)
10.6.5 API Gateway как single point of failure
API Gateway — критический компонент: если он недоступен, весь API недоступен. Меры:
-
Высокая доступность. Минимум 2 инстанса за балансировщиком (ALB/NLB). В Kubernetes — несколько реплик с anti-affinity.
-
Минимальная логика. Gateway должен быть «тонким» — маршрутизация, аутентификация, rate limiting. Бизнес-логика — только в сервисах. Чем проще Gateway — тем надёжнее.
-
Мониторинг. Латенси Gateway (p50, p99), error rate, throughput, количество активных соединений. Alerting при деградации.
-
Graceful degradation. При сбое одного backend Gateway должен возвращать частичный ответ или fallback, а не 502 на весь запрос:
async def get_dashboard(user_id): # Параллельные запросы с таймаутами user, orders, balance = await asyncio.gather( fetch_user(user_id, timeout=2), fetch_orders(user_id, timeout=2), fetch_balance(user_id, timeout=2), return_exceptions=True ) return { "user": user if not isinstance(user, Exception) else None, "orders": orders if not isinstance(orders, Exception) else None, "balance": balance if not isinstance(balance, Exception) else None, }
10.7 Реальные примеры из индустрии
Stripe: эталон API-дизайна
Stripe API считается одним из лучших в индустрии. Ключевые решения:
- Версионирование по дате (
Stripe-Version: 2025-01-15) — инкрементальные изменения вместо монолитных/v2/ - Idempotency-Key для всех мутирующих операций — повторный запрос безопасен
- Expandable objects —
GET /charges/ch_123?expand[]=customerподгружает связанный объект в одном запросе - Cursor-based пагинация —
starting_afterиending_before - Consistent error format — все ошибки имеют
type,code,message,param - Test mode — тот же API, но с тестовыми ключами (
sk_test_...)
GitHub: REST + GraphQL
GitHub поддерживает два API:
- REST API v3 — классический REST с HATEOAS (ссылки в каждом ответе)
- GraphQL API v4 — клиент запрашивает ровно те поля, которые нужны
GraphQL был введён, потому что REST API требовал множество запросов для получения связанных данных (репозиторий → коммиты → автор → организация). GraphQL позволяет получить всё за один запрос.
Google: API Design Guide
Google опубликовал публичный API Design Guide, используемый для всех Google Cloud API. Ключевые принципы:
- Resource-oriented design — всё является ресурсом с CRUD-операциями
- Standard methods — List, Get, Create, Update, Delete для каждого ресурса
- Custom methods — для операций, не укладывающихся в CRUD:
POST /users/123:ban,POST /documents/456:export - Long-running operations — для асинхронных операций возвращается объект
Operationс методамиgetиcancel
POST /v1/projects/my-project/instances
→ 200 OK
{
"name": "operations/op-123",
"done": false,
"metadata": { ... }
}
GET /v1/operations/op-123
→ 200 OK
{
"name": "operations/op-123",
"done": true,
"response": { ... }
}
Twitter: Rate Limiting по уровням
Twitter API применяет многоуровневый rate limiting:
| Эндпоинт | Лимит | Окно |
|---|---|---|
| GET /tweets (поиск) | 450 запросов | 15 минут |
| POST /tweets (публикация) | 200 твитов | 15 минут |
| GET /users/me | 75 запросов | 15 минут |
| POST /likes | 50 запросов | 15 минут |
Лимиты варьируются по уровню подписки (Free, Basic, Pro, Enterprise), эндпоинту и типу операции.
10.8 Практические рекомендации
Чек-лист при проектировании API
-
Определите ресурсы. Начните с сущностей предметной области. Каждая сущность — ресурс с URL. Используйте существительные во множественном числе (
/users,/orders). -
Выберите формат. JSON — де-факто стандарт. Protobuf/gRPC — для внутренних high-performance API. GraphQL — когда клиент нуждается в гибкости запросов (множество связанных сущностей).
-
Спроектируйте пагинацию. Cursor-based — по умолчанию для новых API. Offset — только для admin-панелей.
-
Реализуйте идемпотентность. Все мутирующие операции (POST, PATCH) должны принимать
Idempotency-Key. Хранилище ключей — Redis или БД с TTL 24 часа. -
Настройте rate limiting. Sliding Window Counter на Redis — оптимальный выбор. Разные лимиты для разных эндпоинтов. Возвращайте
RateLimit-*заголовки. -
Версионируйте с первого дня.
/v1/в URL — простейший подход. Документируйте deprecation policy. Не ломайте обратную совместимость без версии. -
Стандартизируйте ошибки. Единый формат для всех эндпоинтов. Включайте
request_idдля отладки. Используйте правильные HTTP-статус коды. -
Документируйте. OpenAPI (Swagger) — стандарт описания REST API. Генерируйте документацию из спецификации. Предоставляйте примеры запросов и ответов для каждого эндпоинта.
-
API Gateway. Для микросервисной архитектуры — обязателен. Централизуйте аутентификацию, rate limiting, логирование. Держите Gateway «тонким» — без бизнес-логики.
-
Мониторинг. Ключевые метрики:
- Latency (p50, p95, p99) — по эндпоинту
- Error rate (4xx, 5xx) — по эндпоинту и клиенту
- Request rate (RPS) — общий и по клиенту
- Rate limit hits — количество отклонённых запросов (429)
- API version usage — для планирования deprecation
10.9 Вопросы для самопроверки
-
Объясните разницу между PUT и PATCH. Приведите пример, когда использование PUT нежелательно и PATCH — единственный правильный выбор.
-
Ваш API возвращает список из 10 миллионов записей. Сравните offset-based и cursor-based пагинацию для этого случая. Какой подход вы выберете и почему?
-
Объясните проблему «двойного списания» при отсутствии идемпотентности. Как Idempotency-Key решает эту проблему? Как обработать race condition, когда два запроса с одним ключом приходят одновременно?
-
Вы проектируете публичный API для SaaS-платформы. Клиенты — сторонние разработчики. Какую стратегию версионирования вы выберете? Как долго будете поддерживать старые версии?
-
Сравните алгоритмы Token Bucket и Sliding Window Counter. Для какого сценария каждый из них оптимален?
-
Ваш API Gateway обслуживает 50 микросервисов. Один из сервисов начал отвечать медленно (5 секунд вместо 50ms). Как API Gateway должен обработать эту ситуацию? Какие паттерны помогут?
-
Объясните паттерн Backend for Frontend (BFF). Когда он оправдан? Какие недостатки?
-
Вы хотите добавить обязательное поле
phoneв запрос создания пользователя (POST /users). Является ли это breaking change? Как сделать это без нарушения обратной совместимости? -
Спроектируйте rate limiting для API с тремя уровнями подписки: Free (100 req/day), Pro (10K req/day), Enterprise (1M req/day). Какой алгоритм, где хранить состояние, как обрабатывать превышение?
-
Объясните, почему
GET /users?action=delete&id=123— плохой дизайн API. Какие проблемы это может вызвать?
10.10 Дополнительные ресурсы
Papers
- Architectural Styles and the Design of Network-based Software Architectures (Roy Fielding, 2000) — диссертация, определившая REST. Фундаментальная работа, которую стоит прочитать каждому, кто проектирует API.
- RFC 7231: Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content — определение семантики HTTP-методов и статус-кодов. Авторитетный источник для любого спора о «правильном» использовании HTTP.
- RFC 7807: Problem Details for HTTP APIs — стандартизированный формат ошибок для HTTP API.
- RFC 6585: Additional HTTP Status Codes — определяет 429 Too Many Requests и другие полезные статус-коды.
Блоги
- Stripe API Documentation (stripe.com/docs/api) — эталонный пример API-документации: примеры на нескольких языках, idempotency keys, expandable objects, versioning.
- Google Cloud API Design Guide (cloud.google.com/apis/design) — публичный гайд Google по проектированию API. Покрывает naming conventions, standard methods, errors, versioning.
- Microsoft REST API Guidelines (github.com/microsoft/api-guidelines) — детальные рекомендации Microsoft по проектированию REST API, включая пагинацию, версионирование и обработку ошибок.
- Cloudflare Blog: Rate Limiting (blog.cloudflare.com) — практические статьи об алгоритмах rate limiting на масштабе миллиардов запросов.
Talks
- "How to Design a Good API and Why It Matters" (Joshua Bloch, Google) — классическое выступление о принципах проектирования API. Хотя речь о Java API, принципы универсальны.
- "RESTful API Design" (Apigee/Google, YouTube series) — серия видео о best practices REST API от команды Apigee.
- "gRPC vs REST: Understanding the Tradeoffs" (Mark Vinod, QCon) — сравнение REST и gRPC с примерами из production.
- "API Design Patterns" (JJ Geewax, Google) — паттерны проектирования API, описанные в книге того же автора, включая long-running operations, pagination и singleton resources.