← все темы

Проектирование API

Вопросов с разбором: 23. В боте эти же карточки приходят по расписанию повторений.

1 Блок 1 из 5: REST — Что такое REST и какие у него принципы? 🔥
REST (Representational State Transfer) — архитектурный стиль Роя Филдинга для распределённых систем. На практике «REST API» — это HTTP API, построенный вокруг ресурсов.

Принципы (ограничения):
- Клиент — сервер: интерфейс и хранение данных разделены.
- Stateless: каждый запрос содержит всё нужное, сервер не хранит контекст клиента между запросами.
- Кэшируемость: ответы явно помечаются кэшируемыми или нет.
- Единый интерфейс: ресурсы по URL, действия — HTTP-методами, представления (JSON), самоописываемые сообщения, гипермедиа (HATEOAS).
- Многоуровневость: между клиентом и сервером могут быть прокси, кэши, балансировщики.
- Код по требованию (необязательно).

Суть на практике: существительные в URL, глаголы — это HTTP-методы, правильные статус-коды, отсутствие серверных сессий.
2 Блок 1 из 5: REST — Как правильно именовать ресурсы и строить URL?
- Существительные во множественном числе, а не глаголы: /orders, а не /getOrders.
- Коллекция и элемент: /orders и /orders/42.
- Вложенность для принадлежности, не глубже 1–2 уровней: /users/7/orders.
- Нижний регистр и дефисы: /payment-methods.
- Фильтры, сортировка, пагинация — в query-параметрах: /orders?status=paid&sort=-created_at&limit=20.
- Версия — в начале пути: /v1/orders.

Действия, которые плохо ложатся на CRUD, оформляют как подресурс или «ресурс-действие»:
POST /orders/42/cancel        # действие над заказом
POST /orders/42/refunds # создать возврат — новый ресурс


Второй вариант чище: у возврата появляется свой id, статус и история.
3 Блок 1 из 5: REST — Как CRUD ложится на HTTP-методы и чем PUT отличается от PATCH? 🔥
GET    /orders        список              200
POST /orders создать 201 + Location: /orders/42
GET /orders/42 получить 200 / 404
PUT /orders/42 заменить целиком 200 / 204
PATCH /orders/42 изменить частично 200 / 204
DELETE /orders/42 удалить 204 / 404


PUT — клиент присылает полное представление ресурса. Отсутствующее поле означает «очистить». Идемпотентен.

PATCH — клиент присылает только изменения. В общем случае не идемпотентен.

PUT   /users/7  {"name": "Аня", "email": "a@x.ru", "phone": null}
PATCH /users/7 {"phone": "+79990001122"}


Форматы PATCH: JSON Merge Patch (RFC 7396: прислали поле — заменили, прислали null — удалили) и JSON Patch (RFC 6902: список операций add/remove/replace).
4 Блок 1 из 5: REST — Какие статус-коды возвращать из API в типичных ситуациях? 🔥
- 200 OK — успешный GET, PUT, PATCH с телом.
- 201 Created — ресурс создан, плюс заголовок Location.
- 202 Accepted — задача принята в фоновую обработку, результат будет позже.
- 204 No Content — успех без тела (DELETE).
- 400 Bad Request — невалидный JSON, не тот тип параметра.
- 401 Unauthorized — нет или неверный токен.
- 403 Forbidden — токен валиден, но прав нет.
- 404 Not Found — ресурса нет (или скрываем его существование от чужих).
- 409 Conflict — конфликт состояния: дубликат, устаревшая версия, недопустимый переход статуса.
- 422 Unprocessable Entity — синтаксис верный, но данные не прошли валидацию (FastAPI использует его по умолчанию).
- 429 Too Many Requests — превышен лимит, плюс Retry-After.
- 500 — непредвиденная ошибка; 503 — временно недоступен.

Антипаттерн: всегда 200 и {"success": false} в теле — ломает кэши, ретраи, мониторинг и клиентские библиотеки.
5 Блок 1 из 5: REST — Как оформлять ошибки в API?
Ошибки должны быть единообразными, машиночитаемыми и понятными человеку. Стандарт — Problem Details (RFC 9457, ранее RFC 7807), Content-Type: application/problem+json:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
"type": "https://api.example.com/errors/validation",
"title": "Данные не прошли проверку",
"status": 422,
"detail": "Количество должно быть больше нуля",
"instance": "/orders",
"errors": [{"field": "qty", "code": "min_value"}]
}


Правила:
- стабильный машинный код ошибки — клиент ветвится по нему, а не по тексту;
- ошибки валидации — по полям;
- никаких стек-трейсов, SQL и внутренних деталей в ответе;
- request_id в ответе или заголовке, чтобы найти запрос в логах.
6 Блок 2 из 5: Проектирование — Чем отличается пагинация offset и cursor? 🔥
Offset/limit: GET /orders?limit=20&offset=40 → LIMIT 20 OFFSET 40.
- ✅ просто, можно перейти на любую страницу, легко показать общее количество;
- ❌ на больших offset медленно: БД читает и отбрасывает все предыдущие строки;
- ❌ при вставке и удалении записи «съезжают»: дубли или пропуски между страницами.

Cursor (keyset): GET /orders?limit=20&after=eyJpZCI6MTAwfQ
SELECT * FROM orders
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;

- ✅ стабильная скорость на любой глубине (работает по индексу);
- ✅ нет дублей и пропусков при изменении данных;
- ❌ нельзя перейти сразу на страницу N; курсор непрозрачен.

Когда что: админки и небольшие таблицы — offset; ленты, бесконечный скролл, большие таблицы и публичные API — cursor. Сортировка обязательно с уникальным ключом (id) на конце, иначе порядок недетерминирован.
7 Блок 2 из 5: Проектирование — Как версионировать API? 🔥
Способы:
- В пути: /v1/orders — самый распространённый: видно, легко маршрутизировать и кэшировать.
- В заголовке: Accept: application/vnd.example.v2+json или API-Version: 2 — чистые URL, но сложнее отлаживать.
- В query: ?version=2 — просто, но засоряет параметры.
- По дате (как у Stripe): Stripe-Version: 2024-06-20, версия закрепляется за клиентом.

Главное — не способ, а политика:
- новая мажорная версия только для ломающих изменений;
- совместимые изменения (новое необязательное поле, новый эндпоинт) — без новой версии;
- старую версию объявляют устаревшей (заголовки Deprecation/Sunset), дают срок и следят, кто ещё ей пользуется.
8 Блок 2 из 5: Проектирование — Какие изменения API ломают обратную совместимость?
Ломающие:
- удалить или переименовать поле, эндпоинт, параметр;
- изменить тип поля ("42" → 42) или формат (дата, деньги);
- сделать необязательное поле обязательным во входных данных;
- ужесточить валидацию;
- изменить смысл статус-кода или значения enum;
- изменить семантику поведения (ресурс перестал создаваться синхронно).

Безопасные:
- добавить новый эндпоинт;
- добавить необязательный параметр или поле во входных данных;
- добавить поле в ответ.

Нюанс: добавление значения в enum или поля в ответ безопасно только для «терпимых» клиентов (принцип Постеля). Документируйте, что клиент должен игнорировать неизвестные поля и значения.
9 Блок 2 из 5: Проектирование — Что такое идемпотентность API и как работает Idempotency-Key? 🔥
Проблема: клиент отправил POST /payments, ответ потерялся по таймауту. Платёж прошёл или нет? Повторить — рискуем списать дважды.

Решение: клиент генерирует уникальный ключ на операцию и передаёт его в заголовке:
POST /payments
Idempotency-Key: 6f1c2b8e-7e9a-4a0f-9a55-1d2b3c4d5e6f


Что делает сервер:
1. Ищет ключ в хранилище (таблица с уникальным индексом или Redis).
2. Нет ключа — атомарно резервирует его, выполняет операцию, сохраняет ответ.
3. Ключ есть и операция завершена — возвращает сохранённый ответ, не выполняя операцию заново.
4. Ключ есть, но операция ещё идёт — 409 Conflict.
5. Тот же ключ с другим телом — ошибка (422).

Детали: ключ хранят ограниченное время (например, 24 часа), привязывают к пользователю, резервирование делают атомарно (INSERT ... ON CONFLICT DO NOTHING). Так устроены API Stripe и многих платёжных систем.
10 Блок 2 из 5: Проектирование — Как в API обрабатывать долгие операции?
Если операция идёт дольше пары секунд (отчёт, импорт, генерация видео), держать HTTP-запрос открытым плохо: таймауты прокси, занятые воркеры, обрывы связи.

Паттерн асинхронной операции:
POST /reports            → 202 Accepted
Location: /reports/jobs/77

GET /reports/jobs/77 → 200 {"status": "running", "progress": 40}
GET /reports/jobs/77 → 200 {"status": "done", "result_url": "/reports/77.csv"}


- работа уходит в фоновую очередь (Celery, Kafka);
- клиент опрашивает статус или получает уведомление: webhook, SSE, WebSocket;
- у задачи явные статусы: pending, running, done, failed (с причиной);
- создание задачи делают идемпотентным, чтобы повтор не запустил вторую.
11 Блок 2 из 5: Проектирование — Как проектировать фильтрацию, сортировку и выбор полей?
GET /orders?status=paid&created_after=2026-01-01&sort=-created_at,id&fields=id,total&limit=50


- Фильтры — простые параметры по полям (status=paid), диапазоны через суффиксы (created_after, price_min) или операторы (price[gte]=100).
- Сортировка — sort=-created_at: минус означает «по убыванию».
- Выбор полей — fields=id,total, чтобы не гонять лишние данные.
- Связанные данные — expand=customer или include=items вместо N запросов.

Важно на бэкенде:
- разрешать сортировку и фильтры только по белому списку полей — иначе SQL-инъекции и тяжёлые запросы без индексов;
- ограничивать limit сверху;
- под частые фильтры заводить индексы.
12 Блок 3 из 5: Аутентификация и защита — Чем аутентификация отличается от авторизации? 🔥
- Аутентификация (authentication, AuthN) — кто вы? Проверка личности: пароль, токен, сертификат, OTP. Ошибка — 401 Unauthorized.
- Авторизация (authorization, AuthZ) — что вам можно? Проверка прав на действие или ресурс. Ошибка — 403 Forbidden.

Сначала аутентификация, потом авторизация.

Модели авторизации:
- RBAC — по ролям (admin, manager, user);
- ABAC — по атрибутам (владелец ресурса, отдел, время);
- проверки на уровне объекта: «пользователь видит только свои заказы».

Частая уязвимость — IDOR / BOLA: GET /orders/42 отдаёт чужой заказ, потому что проверили только, что пользователь залогинен. Правильно — фильтровать по владельцу: Order.objects.get(id=42, user=request.user).
13 Блок 3 из 5: Аутентификация и защита — Чем отличаются сессии и токены?
Сессии (stateful): после логина сервер создаёт запись сессии (в БД или Redis), а клиенту отдаёт её id в cookie.
- ✅ сессию легко отозвать — удалить запись;
- ✅ в cookie только случайный id;
- ❌ хранилище сессий нужно на каждом запросе, его нужно масштабировать;
- ❌ cookie неудобны для мобильных клиентов и других доменов; нужна защита от CSRF.

Токены (часто stateless, JWT): сервер выдаёт подписанный токен с данными пользователя, клиент шлёт его в Authorization: Bearer.
- ✅ проверяется без обращения к хранилищу — удобно для микросервисов;
- ✅ подходит для мобильных приложений и межсервисных вызовов;
- ❌ отозвать до истечения срока сложно → короткий срок жизни плюс refresh-токены или чёрный список.

Практика: веб-приложение на одном домене — сессии; публичный API, мобильные клиенты, микросервисы — токены.
14 Блок 3 из 5: Аутентификация и защита — Как устроен JWT и какие у него подводные камни? 🔥
JWT (JSON Web Token) — три части в base64url через точку: header.payload.signature.

header:   {"alg": "RS256", "typ": "JWT"}
payload: {"sub": "42", "role": "user", "exp": 1767225600}
signature: подпись header и payload ключом сервера


- Payload не зашифрован, а только подписан: прочитать может любой, подделать без ключа нельзя. Секреты в JWT не кладут.
- HS256 — общий секрет (подписывает и проверяет один сервис); RS256/ES256 — подписывает приватный ключ, проверяют все по публичному (удобно в микросервисах).

Access и refresh: короткий access-токен (5–15 минут) на каждый запрос и долгий refresh-токен, которым получают новый access. Refresh хранится на сервере и его можно отозвать.

Подводные камни:
- обязательно проверять подпись, exp, aud, iss и жёстко задавать алгоритм — атака alg: none;
- нельзя мгновенно отозвать access-токен;
- в localStorage токен доступен для XSS; безопаснее HttpOnly-cookie;
- большой payload раздувает каждый запрос.
15 Блок 3 из 5: Аутентификация и защита — Что такое OAuth 2.0 и OpenID Connect? 🔥
OAuth 2.0 — протокол делегирования доступа: пользователь разрешает приложению действовать от его имени в другом сервисе, не отдавая пароль. Результат — access token.

Роли: resource owner (пользователь), client (наше приложение), authorization server (выдаёт токены), resource server (API с данными).

Основные flow:
- Authorization Code + PKCE — для веба и мобильных: пользователя перенаправляют на сервер авторизации, он логинится, приложение получает код и обменивает его на токен. Стандарт по умолчанию.
- Client Credentials — сервис ходит в сервис от своего имени, без пользователя.
- Implicit и Password — устарели, не используются.

OpenID Connect (OIDC) — надстройка над OAuth 2.0 для аутентификации: дополнительно выдаётся ID token (JWT с данными пользователя). Это «Войти через Google».

Коротко: OAuth 2.0 — про доступ («что можно»), OIDC — про личность («кто это»).
16 Блок 3 из 5: Аутентификация и защита — Что такое rate limiting и какие есть алгоритмы? 🔥
Rate limiting — ограничение числа запросов от клиента за период. Защищает от перегрузки, перебора паролей, злоупотреблений и помогает честно делить ресурсы.

Алгоритмы:
- Fixed window — счётчик на окно («100 в минуту»). Просто, но на границе окон можно сделать 200 запросов за секунду.
- Sliding window — окно скользит (лог запросов или взвешенная сумма двух окон). Точнее.
- Token bucket — ведро пополняется с постоянной скоростью, запрос тратит токен. Разрешает короткие всплески. Самый популярный.
- Leaky bucket — запросы вытекают с постоянной скоростью, сглаживает поток.

Реализация: счётчики в Redis (INCR + EXPIRE или Lua-скрипт для атомарности), лимиты на nginx или API-шлюзе.

Ответ клиенту: 429 Too Many Requests, Retry-After и заголовки с остатком лимита (RateLimit-Remaining). Лимиты задают по пользователю или API-ключу, а не только по IP.
17 Блок 4 из 5: Альтернативы REST — Что такое gRPC и когда его использовать? 🔥
gRPC — RPC-фреймворк от Google: клиент вызывает метод удалённого сервиса как локальную функцию.

- Контракт описывается в Protobuf (.proto), из него генерируется код клиента и сервера.
- Бинарная сериализация — компактнее и быстрее JSON.
- Работает поверх HTTP/2: мультиплексирование и стриминг (серверный, клиентский, двунаправленный).
- Строгая типизация, дедлайны и отмена вызовов встроены в протокол.

service OrderService {
rpc GetOrder (GetOrderRequest) returns (Order);
rpc WatchOrders (WatchRequest) returns (stream OrderEvent);
}


Когда: межсервисное взаимодействие внутри компании, высокая нагрузка и низкая задержка, стриминг, polyglot-среда с общим контрактом.

Минусы: браузер не умеет gRPC напрямую (нужен gRPC-Web или шлюз), бинарный трафик сложнее отлаживать, нужна генерация кода.
18 Блок 4 из 5: Альтернативы REST — Что такое GraphQL и какие у него плюсы и минусы?
GraphQL — язык запросов к API: один эндпоинт, клиент сам описывает, какие данные и в какой форме ему нужны.

query {
order(id: 42) {
total
customer { name }
items { product { title } qty }
}
}


Плюсы:
- нет over-fetching и under-fetching: ровно нужные поля за один запрос;
- строгая схема и типы, интроспекция и автодокументация;
- удобно для фронтенда с разными экранами и мобильных клиентов.

Минусы:
- проблема N+1 на бэкенде → нужен DataLoader (батчинг);
- сложно ограничить тяжёлые запросы: глубина, стоимость, лимиты;
- HTTP-кэширование почти не работает (всё через POST на один URL);
- ошибки возвращаются со статусом 200 внутри ответа.

Когда: много разных клиентов с разными потребностями, богатая связанная модель данных, BFF для фронтенда.
19 Блок 4 из 5: Альтернативы REST — Как выбрать между REST, gRPC и GraphQL? 🔥
- REST — публичные API, интеграции с партнёрами, простые CRUD-сервисы, когда важны HTTP-кэширование и понятность. Выбор по умолчанию.
- gRPC — внутренние вызовы между микросервисами, высокая нагрузка, стриминг, строгий контракт между командами.
- GraphQL — API для собственных фронтендов и мобильных приложений со сложными и меняющимися требованиями к данным.

Частая комбинация: gRPC между сервисами внутри, REST или GraphQL наружу (через API-шлюз или BFF).

Что сказать на интервью: выбор зависит от потребителей API, требований к производительности, кэшированию и зрелости команды. «Серебряной пули» нет.
20 Блок 4 из 5: Альтернативы REST — Что такое webhook и как сделать его надёжным?
Webhook — обратный вызов: при событии сервис сам отправляет HTTP-запрос на URL, который заранее зарегистрировал клиент. Вместо того чтобы клиент постоянно спрашивал «оплата прошла?», платёжная система сама сообщает об этом.

Отправителю:
- ретраи с экспоненциальной задержкой, если получатель не ответил 2xx;
- подпись запроса (HMAC в заголовке), чтобы получатель мог проверить источник;
- id события и время, журнал доставок, возможность переотправить.

Получателю:
- проверять подпись и время (защита от подделки и повтора);
- быстро отвечать 2xx, а обработку отправлять в очередь;
- быть идемпотентным: одно событие может прийти несколько раз;
- не полагаться на порядок событий — при сомнениях перезапрашивать актуальное состояние через API.
21 Блок 5 из 5: Документация и практика — Что такое OpenAPI и зачем он нужен?
OpenAPI (бывший Swagger) — стандарт машиночитаемого описания HTTP API в YAML или JSON: эндпоинты, параметры, схемы запросов и ответов, ошибки, аутентификация.

Что даёт:
- интерактивная документация (Swagger UI, Redoc);
- генерация клиентов и SDK на разных языках;
- валидация запросов, контрактные тесты, моки;
- единый контракт между бэкендом и фронтендом.

Подходы:
- Code-first — схема генерируется из кода: FastAPI из Pydantic-моделей и аннотаций, в DRF — drf-spectacular.
- Design-first — сначала пишут и согласуют спецификацию, потом код. Удобно, когда API делают несколько команд параллельно.
22 Блок 5 из 5: Документация и практика — Что такое HATEOAS и модель зрелости Ричардсона?
Модель зрелости Ричардсона — уровни «RESTовости» API:
- Уровень 0 — один URL, всё через POST (RPC поверх HTTP, SOAP).
- Уровень 1 — ресурсы: разные URL для разных сущностей.
- Уровень 2 — HTTP-методы и статус-коды по назначению. Здесь находится большинство «REST API».
- Уровень 3 — HATEOAS: ответы содержат ссылки на возможные действия.

{
"id": 42,
"status": "new",
"_links": {
"self": {"href": "/orders/42"},
"pay": {"href": "/orders/42/payments", "method": "POST"},
"cancel": {"href": "/orders/42/cancel", "method": "POST"}
}
}


Идея HATEOAS: клиент не зашивает URL и бизнес-правила, а следует ссылкам от сервера. На практике применяется редко: клиенты всё равно пишутся под конкретный API.
23 Блок 5 из 5: Документация и практика — Как спроектировать API для бронирования столиков в ресторане? 🔥
1. Ресурсы: рестораны, столики, слоты (свободное время), бронирования.

2. Эндпоинты:
GET    /v1/restaurants?city=perm&cursor=...
GET /v1/restaurants/12/availability?date=2026-10-20&guests=4
POST /v1/bookings Idempotency-Key: ...
GET /v1/bookings/{id}
GET /v1/users/me/bookings?status=upcoming
POST /v1/bookings/{id}/cancel


3. Создание брони:
POST /v1/bookings
{"restaurant_id": 12, "starts_at": "2026-10-20T19:00:00+05:00", "guests": 4}

201 Created
Location: /v1/bookings/981
{"id": 981, "status": "confirmed", "table_id": 7, ...}


4. Ошибки: 409 — слот уже занят; 422 — время в прошлом или гостей больше вместимости; 403 — отмена чужой брони; 429 — слишком много попыток.

5. На что обратить внимание:
- двойное бронирование — уникальное ограничение или блокировка в БД, а не проверка в коде;
- Idempotency-Key на создание — защита от повторной отправки;
- время в ISO 8601 с часовым поясом;
- отмена как действие (/cancel), а не DELETE — бронь остаётся в истории;
- уведомление ресторана — асинхронно через очередь или webhook.

Хотите не просто прочитать, а запомнить?

Бот вернёт вам эти вопросы ровно тогда, когда вы начнёте их забывать.