Продвинутый курс по API для Middle/Senior Backend-разработчика

Глубокое погружение в проектирование, безопасность и масштабирование API для успешного прохождения технических собеседований на позиции Middle и Senior Backend. Курс охватывает REST, GraphQL, gRPC, Async API, архитектурные паттерны и реальные кейсы из интервью.

1. Основы REST API и принципы проектирования

Основы REST API и принципы проектирования

REST (Representational State Transfer) — это архитектурный стиль, описанный Роем Филдингом в его докторской диссертации 2000 года. Важно понимать: REST — не протокол и не стандарт, а набор ограничений (constraints), которым должна следовать система, чтобы называться RESTful. Нарушение хотя бы одного из них технически выводит API за рамки REST.

Шесть архитектурных ограничений REST

Клиент-серверная архитектура (Client-Server) разделяет ответственность: клиент управляет пользовательским интерфейсом и состоянием сессии, сервер — хранением данных и бизнес-логикой. Это позволяет масштабировать их независимо. Например, мобильное приложение и веб-интерфейс могут использовать один и тот же API, не зная ничего о внутреннем устройстве сервера.

Отсутствие состояния (Statelessness) означает, что каждый запрос от клиента должен содержать всю информацию, необходимую для его обработки. Сервер не хранит контекст между запросами. Практическое следствие: токен авторизации передаётся в каждом запросе, а не хранится в серверной сессии. Это упрощает горизонтальное масштабирование — любой узел кластера может обработать любой запрос.

Кэшируемость (Cacheability) требует, чтобы ответы явно помечались как кэшируемые или нет. Заголовки Cache-Control, ETag, Last-Modified — это прямое следствие данного ограничения. Правильная кэшируемость снижает нагрузку на сервер и уменьшает задержку для клиента.

Единообразный интерфейс (Uniform Interface) — ключевое ограничение, отличающее REST от других стилей. Оно включает четыре подпринципа: идентификация ресурсов через URI, манипуляция ресурсами через представления, самоописывающие сообщения и HATEOAS (Hypermedia as the Engine of Application State).

Многоуровневая система (Layered System) позволяет вставлять промежуточные слои — балансировщики нагрузки, кэши, шлюзы — между клиентом и сервером. Клиент не знает, с чем именно он общается напрямую.

Код по требованию (Code on Demand) — единственное необязательное ограничение. Сервер может передавать исполняемый код клиенту (например, JavaScript). На практике используется редко в контексте API.

Ресурсы как центральная концепция

В REST всё является ресурсом — сущностью, которую можно идентифицировать, именовать и представить. Ресурс — это не таблица в базе данных и не объект в коде, а концептуальная сущность предметной области.

Ключевое различие между ресурсом и его представлением (representation): ресурс User существует концептуально, а его представление — это JSON или XML, который возвращает сервер. Один ресурс может иметь несколько представлений, выбираемых через механизм content negotiation с помощью заголовка Accept.

Проектирование URI: правила и типичные ошибки

URI (Uniform Resource Identifier) должен идентифицировать ресурс, а не действие. Это принципиальное отличие REST от RPC-стиля.

| Плохо (RPC-стиль) | Хорошо (REST-стиль) | |---|---| | POST /getUser | GET /users/42 | | POST /createOrder | POST /orders | | GET /deleteProduct?id=5 | DELETE /products/5 | | POST /updateUserStatus | PATCH /users/42 | | GET /getUserOrders?userId=42 | GET /users/42/orders |

Правила именования URI:

  • Используй существительные во множественном числе: /users, /orders, /products
  • Иерархия отражает отношения: /users/42/orders/7 — заказ №7 пользователя №42
  • Строчные буквы и дефисы вместо подчёркиваний: /product-categories, не /product_categories
  • Без глаголов в URI: действие выражается HTTP-методом
  • Без расширений файлов: /users/42, не /users/42.json
  • Типичная ошибка на собеседовании — смешивать уровни вложенности. Если заказ может существовать независимо от пользователя, лучше использовать /orders/7, а не всегда требовать /users/42/orders/7. Глубокая вложенность (более 2–3 уровней) усложняет API и часто указывает на проблемы в дизайне.

    HTTP-методы и их семантика

    Каждый HTTP-метод несёт строгую семантику, которую нельзя нарушать произвольно.

    GET — получение ресурса. Безопасный (safe) и идемпотентный. Не должен изменять состояние сервера. Может кэшироваться.

    POST — создание нового ресурса или выполнение действия. Не идемпотентный: два одинаковых POST-запроса создадут два ресурса. Ответ на успешное создание — 201 Created с заголовком Location, указывающим на новый ресурс.

    PUT — полная замена ресурса. Идемпотентный: повторный PUT с теми же данными даёт тот же результат. Клиент передаёт полное представление ресурса. Если поле не передано — оно обнуляется.

    PATCH — частичное обновление. Клиент передаёт только изменяемые поля. Технически не обязан быть идемпотентным (хотя на практике часто является).

    DELETE — удаление ресурса. Идемпотентный: повторное удаление уже удалённого ресурса возвращает 404, но состояние системы не меняется.

    Коды HTTP-ответов: точность имеет значение

    Правильное использование кодов ответа — признак зрелого API. Распространённая ошибка — возвращать 200 OK с телом {"error": "not found"}. Это ломает все инструменты мониторинга и клиентские библиотеки.

    | Диапазон | Смысл | Примеры | |---|---|---| | 2xx | Успех | 200 OK, 201 Created, 204 No Content | | 3xx | Перенаправление | 301 Moved Permanently, 304 Not Modified | | 4xx | Ошибка клиента | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity | | 5xx | Ошибка сервера | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable |

    Тонкие различия, которые проверяют на собеседованиях:

  • 401 Unauthorized — клиент не аутентифицирован (несмотря на название, это про authentication)
  • 403 Forbidden — клиент аутентифицирован, но не имеет прав (authorization)
  • 404 Not Found — ресурс не существует, или сервер намеренно скрывает его существование
  • 409 Conflict — конфликт состояния (например, попытка создать пользователя с уже существующим email)
  • 422 Unprocessable Entity — запрос синтаксически корректен, но семантически невалиден (например, дата окончания раньше даты начала)
  • Принципы проектирования: что отличает хороший API

    Консистентность — самое важное свойство. Если в одном эндпоинте поле называется userId, в другом оно не должно называться user_id или uid. Если пагинация реализована через page/per_page в одном месте, она должна быть такой же везде.

    Принцип наименьшего удивления (Principle of Least Astonishment): поведение API должно соответствовать ожиданиям разработчика. Если DELETE /users/42 возвращает тело с данными удалённого пользователя — это удивит большинство клиентов. Стандартное поведение — 204 No Content.

    Явность над неявностью: лучше требовать явного указания параметров, чем полагаться на дефолтные значения, которые клиент может не знать. Например, если API по умолчанию возвращает только активных пользователей, это должно быть задокументировано и, желательно, управляться явным параметром ?status=active.

    Обратная совместимость (backward compatibility): изменения в API не должны ломать существующих клиентов. Добавление нового поля в ответ — безопасно. Удаление поля или изменение его типа — breaking change, требующий версионирования.

    > Хороший API — это продукт. Он должен быть спроектирован с точки зрения разработчика, который будет его использовать, а не с точки зрения разработчика, который его создаёт.

    Реальный пример плохого дизайна: API возвращает список заказов, но статус заказа кодируется числом (1 — новый, 2 — в обработке, 3 — доставлен). Клиент вынужден хранить маппинг. Лучше возвращать строку "status": "processing" — это самодокументируемо и устойчиво к добавлению новых статусов.

    !Архитектура REST API: клиент, сервер, ресурсы и HTTP-методы

    HATEOAS на практике

    HATEOAS — самое часто игнорируемое ограничение REST. Идея: ответ сервера должен содержать ссылки на связанные действия, чтобы клиент мог навигировать по API, не зная его структуры заранее.

    На практике полный HATEOAS редко реализуется из-за сложности поддержки. Но частичная реализация — включение ссылок на связанные ресурсы — встречается в зрелых API (GitHub API, Stripe API). На собеседовании важно знать концепцию и уметь объяснить, почему большинство "REST API" на самом деле не являются полностью RESTful.

    10. gRPC: протокол, protobuf, streaming и сервисные контракты

    gRPC: протокол, protobuf, streaming и сервисные контракты

    Что такое gRPC и почему он важен

    gRPC (gRPC Remote Procedure Call) — высокопроизводительный фреймворк для удалённых вызовов процедур, разработанный Google и открытый в 2015 году. Использует HTTP/2 как транспорт и Protocol Buffers (protobuf) как формат сериализации.

    Ключевые преимущества перед REST+JSON:

  • Бинарная сериализация protobuf в 3–10 раз компактнее JSON
  • HTTP/2 мультиплексирование — несколько запросов по одному соединению
  • Строгая типизация через схему — ошибки типов обнаруживаются на этапе компиляции
  • Встроенная поддержка streaming (4 типа)
  • Автогенерация клиентского и серверного кода для 10+ языков
  • Protocol Buffers: язык определения схемы

    Protocol Buffers (protobuf) — бинарный формат сериализации и язык описания схемы данных. .proto файл — это контракт сервиса.

    Нумерация полей и совместимость

    Числа в protobuf (1, 2, 3...) — это теги полей, а не порядковые номера. Именно они используются при сериализации, а не имена полей. Это ключевое отличие от JSON.

    Правила обратной совместимости:

  • Никогда не меняй тег существующего поля
  • Никогда не меняй тип существующего поля
  • Можно добавлять новые поля с новыми тегами
  • Удалённые теги нужно резервировать: reserved 4, 5; reserved "old_field";
  • Поля 1–15 кодируются одним байтом, 16–2047 — двумя байтами. Часто используемые поля должны иметь теги 1–15.

    Четыре типа gRPC streaming

    Это одно из главных преимуществ gRPC перед REST.

    Unary RPC

    Классический запрос-ответ, как в REST:

    Server Streaming

    Сервер отправляет поток ответов на один запрос. Пример: подписка на обновления, экспорт большого файла:

    Client Streaming

    Клиент отправляет поток запросов, сервер возвращает один ответ. Пример: загрузка файла по частям:

    Bidirectional Streaming

    Оба направления — потоки. Пример: чат, real-time collaboration:

    Metadata, Interceptors и Error Handling

    Metadata в gRPC — аналог HTTP-заголовков. Используется для передачи токенов, trace ID, tenant ID:

    Interceptors — аналог middleware для gRPC:

    Коды ошибок gRPC — стандартизированный набор из 16 кодов:

    | Код | Название | HTTP-аналог | |---|---|---| | 0 | OK | 200 | | 1 | CANCELLED | — | | 2 | UNKNOWN | 500 | | 3 | INVALID_ARGUMENT | 400 | | 4 | DEADLINE_EXCEEDED | 504 | | 5 | NOT_FOUND | 404 | | 7 | PERMISSION_DENIED | 403 | | 8 | RESOURCE_EXHAUSTED | 429 | | 12 | UNIMPLEMENTED | 501 | | 14 | UNAVAILABLE | 503 | | 16 | UNAUTHENTICATED | 401 |

    gRPC vs REST vs GraphQL: когда что выбирать

    | Критерий | REST | GraphQL | gRPC | |---|---|---|---| | Транспорт | HTTP/1.1, HTTP/2 | HTTP/1.1, HTTP/2 | HTTP/2 | | Формат | JSON, XML | JSON | Protobuf (бинарный) | | Типизация | Нет (OpenAPI опционально) | Строгая схема | Строгая схема | | Streaming | Ограниченно (SSE) | Subscriptions | 4 типа | | Браузер | Нативно | Нативно | Через gRPC-Web | | Кэширование | Отличное (HTTP) | Сложное | Нет | | Производительность | Средняя | Средняя | Высокая | | Лучше для | Публичных API | Сложных клиентов | Микросервисов |

    Выбирай gRPC когда:

  • Внутренняя коммуникация микросервисов
  • Нужна максимальная производительность (высокий RPS, низкая задержка)
  • Streaming данных в реальном времени
  • Строгая типизация критична (финансы, медицина)
  • Полиглотная среда (сервисы на разных языках)
  • Не выбирай gRPC когда:

  • Публичный API (браузерные клиенты без gRPC-Web)
  • Нужно простое кэширование через CDN
  • Команда не знакома с protobuf
  • Требуется человекочитаемый формат для отладки
  • !Сравнение архитектур gRPC и REST: транспорт, сериализация и типы взаимодействия

    11. Async API: WebSocket, SSE, Long Polling и AsyncAPI

    Async API: WebSocket, SSE, Long Polling и AsyncAPI

    Когда нужны асинхронные API

    Классический REST работает по модели запрос-ответ: клиент инициирует запрос, сервер отвечает. Это не подходит для сценариев, где сервер должен push данные клиенту без явного запроса: чаты, уведомления, биржевые котировки, статус выполнения долгих операций, совместное редактирование документов.

    Три основных подхода к реализации server push, каждый со своими trade-off'ами.

    Long Polling: простейший подход

    Long Polling — клиент отправляет обычный HTTP-запрос, но сервер не отвечает немедленно. Он держит соединение открытым до появления новых данных или таймаута. Получив ответ, клиент немедленно отправляет новый запрос.

    AsyncAPI поддерживает генерацию документации (AsyncAPI Studio), mock-серверов и клиентского кода — аналогично OpenAPI для REST.

    !Сравнение Long Polling, SSE и WebSocket: задержка, нагрузка и направление данных

    12. Архитектурные паттерны: API Gateway и Backend for Frontend

    Архитектурные паттерны: API Gateway и Backend for Frontend

    API Gateway: единая точка входа

    API Gateway — это сервис, который является единой точкой входа для всех клиентских запросов к микросервисной архитектуре. Вместо того чтобы клиент знал адреса десятков микросервисов, он общается только с Gateway.

    Без API Gateway клиент вынужден:

  • Знать адреса всех микросервисов
  • Самостоятельно агрегировать данные из нескольких сервисов
  • Реализовывать аутентификацию для каждого сервиса отдельно
  • Обрабатывать разные форматы ответов
  • Функции API Gateway

    Маршрутизация (routing) — перенаправление запросов к нужному микросервису:

    Аутентификация и авторизация — централизованная проверка токенов. Микросервисы доверяют Gateway и не проверяют токены самостоятельно (или проверяют только подпись JWT):

    Трансформация запросов и ответов — изменение формата данных, добавление/удаление заголовков:

    Агрегация запросов (request aggregation) — один запрос клиента превращается в несколько запросов к микросервисам:

    Circuit Breaker — защита от каскадных отказов. Если микросервис недоступен, Gateway перестаёт отправлять к нему запросы и возвращает fallback-ответ:

    Популярные решения

    | Решение | Тип | Особенности | |---|---|---| | Kong | Open Source / Enterprise | Lua-плагины, высокая производительность | | AWS API Gateway | Managed | Интеграция с AWS Lambda, WebSocket | | Nginx | Open Source | Простота, высокая производительность | | Traefik | Open Source | Автообнаружение сервисов, Kubernetes | | Envoy | Open Source | Service mesh, gRPC, xDS | | Azure API Management | Managed | Портал разработчика, аналитика |

    Backend for Frontend (BFF)

    BFF (Backend for Frontend) — паттерн, при котором для каждого типа клиента создаётся отдельный backend-сервис, оптимизированный под нужды этого клиента.

    Проблема, которую решает BFF: разные клиенты имеют разные потребности. Мобильное приложение работает на медленном соединении и нуждается в минимальном объёме данных. Веб-приложение может позволить себе больше данных и сложные запросы. Smart TV имеет ограниченные возможности рендеринга.

    Если использовать один общий API для всех клиентов, он становится компромиссом — слишком тяжёлым для мобильных, слишком ограниченным для веба.

    Что делает BFF

    Агрегация данных — BFF знает, какие данные нужны конкретному клиенту:

    Адаптация протокола — BFF может использовать gRPC для общения с микросервисами и REST/GraphQL для клиентов:

    BFF vs API Gateway: ключевые различия

    Это частый вопрос на собеседованиях. Многие путают эти паттерны.

    | Аспект | API Gateway | BFF | |---|---|---| | Назначение | Инфраструктурный слой | Бизнес-логика для клиента | | Кто владеет | Платформенная команда | Команда конкретного клиента | | Логика | Маршрутизация, auth, rate limiting | Агрегация, трансформация, бизнес-правила | | Количество | Один (или несколько для регионов) | По одному на тип клиента | | Изменения | Редко | Часто (вместе с клиентом) |

    На практике API Gateway и BFF используются вместе: Gateway — первый слой (auth, rate limiting, SSL termination), BFF — второй слой (агрегация под конкретного клиента).

    Когда BFF не нужен

    BFF добавляет операционную сложность: нужно поддерживать несколько сервисов, деплоить их независимо, следить за их здоровьем. Это оправдано, когда:

  • Клиенты имеют существенно разные требования к данным
  • Команды клиентов работают независимо и хотят контролировать свой BFF
  • Нужна оптимизация под конкретный клиент (мобильный трафик, latency)
  • Если у вас один тип клиента или требования клиентов похожи — достаточно одного API Gateway.

    !Архитектура API Gateway и BFF: маршрутизация запросов от разных клиентов к микросервисам

    13. Event-driven architecture и CQRS в контексте API

    Event-driven architecture и CQRS в контексте API

    Event-Driven Architecture: основные концепции

    Event-Driven Architecture (EDA, событийно-ориентированная архитектура) — архитектурный стиль, в котором компоненты системы взаимодействуют через события, а не через прямые вызовы. Вместо OrderService.createOrder()InventoryService.reserveItems() система работает так: OrderService публикует событие OrderCreated, InventoryService подписывается на него и реагирует.

    Событие (event) — неизменяемый факт о том, что произошло в системе. Три типа сообщений в EDA:

  • Event — "что-то произошло": OrderCreated, PaymentProcessed, UserRegistered
  • Command — "сделай что-то": CreateOrder, ProcessPayment, SendEmail
  • Query — "расскажи мне что-то": GetOrderStatus, GetUserProfile
  • Ключевое свойство событий: они описывают прошлое, их нельзя изменить. OrderCreated уже произошло — это факт.

    Преимущества EDA

    Слабая связанность (loose coupling): OrderService не знает о существовании InventoryService. Можно добавить новый подписчик (EmailService для отправки подтверждения) без изменения OrderService.

    Масштабируемость: подписчики масштабируются независимо. Если обработка платежей медленная — добавляем больше экземпляров PaymentService.

    Устойчивость к отказам: если InventoryService недоступен, событие остаётся в очереди и будет обработано позже.

    Паттерны EDA

    Pub/Sub (Publish/Subscribe) — издатель публикует событие, все подписчики получают его:

    Event Sourcing — состояние системы хранится как последовательность событий, а не как текущее состояние:

    Преимущества Event Sourcing: полная история изменений, возможность воспроизвести состояние на любой момент времени, упрощённый аудит.

    Saga Pattern: распределённые транзакции

    Saga — паттерн для управления распределёнными транзакциями в микросервисной архитектуре. Когда операция затрагивает несколько сервисов, нельзя использовать обычную ACID-транзакцию. Saga разбивает транзакцию на последовательность локальных транзакций с компенсирующими операциями.

    Пример: оформление заказа в e-commerce:

    Если шаг 3 (оплата) не прошёл, нужно откатить шаги 1 и 2:

    Choreography vs Orchestration

    Choreography (хореография) — каждый сервис знает, что делать при получении события. Нет центрального координатора:

    Orchestration (оркестрация) — центральный сервис (оркестратор) управляет процессом:

    | Аспект | Choreography | Orchestration | |---|---|---| | Связанность | Слабая | Средняя | | Сложность | Распределена по сервисам | Сосредоточена в оркестраторе | | Отладка | Сложная (нет единой точки) | Проще (один сервис) | | Масштабируемость | Лучше | Хуже (оркестратор — узкое место) | | Видимость | Низкая | Высокая |

    CQRS: разделение чтения и записи

    CQRS (Command Query Responsibility Segregation) — паттерн, разделяющий операции чтения (Query) и записи (Command) на разные модели и, опционально, разные хранилища.

    Проблема, которую решает CQRS: в сложных системах требования к чтению и записи часто противоречат друг другу. Запись требует строгой консистентности и транзакций. Чтение требует денормализованных данных, сложных агрегаций, высокой производительности.

    Реализация CQRS в API

    Eventual Consistency в CQRS

    Разделение хранилищ означает eventual consistency: после записи в command-хранилище, read-хранилище обновляется асинхронно. Клиент может получить устаревшие данные сразу после записи.

    Стратегии работы с этим:

  • Optimistic UI: клиент обновляет UI немедленно, не дожидаясь подтверждения от read-модели
  • Read-your-writes: после записи клиент читает из command-хранилища (с задержкой переключается на read-хранилище)
  • Version tokens: ответ на команду содержит версию, клиент передаёт её в query — сервер ждёт, пока read-модель достигнет этой версии
  • Outbox Pattern: надёжная публикация событий

    Классическая проблема: как атомарно сохранить данные в БД и опубликовать событие в Kafka? Если сохранили в БД, но Kafka недоступна — событие потеряно.

    Outbox Pattern решает это:

    Отдельный Message Relay процесс читает таблицу outbox и публикует события в брокер. После успешной публикации — помечает запись как обработанную. Это гарантирует at-least-once delivery.

    !Схема CQRS с Event Sourcing: разделение command и query сторон через event bus

    14. Сложные кейсы проектирования API и анализ trade-off'ов

    Сложные кейсы проектирования API и анализ trade-off'ов

    Кейс 1: API для системы уведомлений в реальном времени

    Задача: спроектировать API для системы уведомлений, которая должна доставлять уведомления 10 миллионам пользователей с задержкой менее 1 секунды. Пользователи могут быть онлайн (мобильное приложение, веб) или офлайн.

    Анализ требований:

  • Онлайн-пользователи: push в реальном времени
  • Офлайн-пользователи: хранение и доставка при подключении
  • Разные типы клиентов: iOS, Android, Web
  • Масштаб: 10M пользователей, пиковая нагрузка — 100K уведомлений/сек
  • Решение:

    API для получения уведомлений:

    Trade-off'ы:

    WebSocket vs SSE: WebSocket двунаправленный (нужен для подтверждения доставки), SSE проще и лучше работает через прокси. Выбор: WebSocket для мобильных (нужно подтверждение), SSE для веба (проще, HTTP/2).

    Хранение: Redis для онлайн-состояния (TTL 30 минут), PostgreSQL для inbox (персистентность), Kafka для буферизации пиков.

    Проблема fan-out: если одно уведомление нужно отправить 1M пользователей (например, системное объявление), наивный подход создаст 1M записей в БД. Решение: хранить одно уведомление с флагом "для всех" и lazy-loading при запросе пользователя.

    Кейс 2: API для финансовых транзакций

    Задача: спроектировать API для перевода денег между счетами. Требования: атомарность, идемпотентность, аудит, высокая доступность.

    Ключевые проблемы:

  • Двойное списание: клиент повторяет запрос при таймауте
  • Частичный отказ: деньги списаны, но не зачислены
  • Конкурентные обновления: два запроса одновременно меняют баланс
  • API:

    Почему 202 Accepted, а не 201 Created? Перевод — асинхронная операция. Немедленный ответ 201 означал бы, что транзакция завершена, но это не так.

    Polling для статуса:

    Реализация атомарности:

    Trade-off: синхронная транзакция vs Saga. Синхронная проще, но не работает при разных БД для разных счетов. Saga сложнее, но масштабируется. Для финансов часто выбирают синхронную транзакцию с одной БД (PostgreSQL) — надёжность важнее масштабируемости.

    Кейс 3: API для поиска с фильтрацией

    Задача: спроектировать API поиска товаров с фильтрами, сортировкой и пагинацией. 10M товаров, 1000 запросов/сек.

    Наивный подход (плохо):

    Проблемы: offset-пагинация медленная на больших данных, нет кэширования (каждый набор фильтров уникален), нет поддержки полнотекстового поиска.

    Лучший подход:

    Почему POST для поиска? Сложные фильтры не помещаются в query string, POST позволяет структурированный JSON. Минус: не кэшируется HTTP-кэшем. Решение: добавить GET /products/search с закодированным состоянием для кэшируемых запросов.

    Архитектура:

    Кэширование: хэш тела запроса как ключ кэша. Популярные запросы (топ-1000 по частоте) кэшируются в Redis на 5 минут.

    Кейс 4: Версионирование при breaking change

    Ситуация: в production работает API v1. Нужно изменить структуру ответа /users/{id} — разбить поле address (строка) на объект с полями street, city, country. Это breaking change.

    Стратегия миграции:

    Внутренняя реализация: не дублируй код. Используй одну бизнес-логику, трансформируй ответ на уровне сериализатора:

    Анализ trade-off'ов: REST vs GraphQL vs gRPC

    На собеседовании часто просят обосновать выбор технологии. Структурированный подход:

    Вопросы для анализа:

  • Кто клиент? (браузер, мобильное приложение, другой сервис)
  • Насколько разнообразны потребности клиентов в данных?
  • Нужен ли streaming?
  • Насколько важна производительность?
  • Какой опыт у команды?
  • Нужен ли публичный API?
  • Реальный кейс: компания строит платформу с мобильным приложением, веб-интерфейсом и партнёрским API.

  • Партнёрский API → REST. Партнёры ожидают REST, хорошая документация через OpenAPI, кэширование через CDN.
  • Мобильное приложение → GraphQL. Разные экраны нуждаются в разных данных, нужно минимизировать трафик.
  • Внутренние микросервисы → gRPC. Высокая производительность, строгая типизация, streaming для real-time данных.
  • Это не противоречие — разные технологии для разных задач. API Gateway и BFF помогают скрыть эту гетерогенность от клиентов.

    !Матрица выбора технологии API: REST, GraphQL, gRPC по ключевым критериям

    15. Подготовка к собеседованиям: типичные вопросы Middle и Senior

    Подготовка к собеседованиям: типичные вопросы Middle и Senior

    Как устроено техническое интервью по API

    Собеседования на позиции Middle и Senior backend-разработчика по теме API обычно состоят из трёх частей: теоретические вопросы (30–40 минут), разбор кейсов и system design (30–40 минут), code review или live coding (20–30 минут). Понимание этой структуры помогает правильно распределить подготовку.

    Ключевое отличие Middle от Senior на интервью: Middle должен знать что и как, Senior — почему и какие trade-off'ы. Интервьюер на Senior-позицию ожидает, что кандидат сам задаёт уточняющие вопросы, обозначает ограничения и предлагает альтернативы.

    Блок 1: Теоретические вопросы Middle-уровня

    "Объясните разницу между PUT и PATCH"

    Слабый ответ: "PUT заменяет весь объект, PATCH — только часть".

    Сильный ответ: PUT — идемпотентная операция полной замены ресурса. Клиент передаёт полное представление, и сервер заменяет ресурс целиком. Если поле не передано — оно обнуляется. PATCH — частичное обновление, клиент передаёт только изменяемые поля. Технически PATCH не обязан быть идемпотентным: {"increment": 1} — не идемпотентная PATCH-операция, а {"status": "active"} — идемпотентная. На практике большинство PATCH-реализаций идемпотентны, но это не гарантировано спецификацией.

    "Что такое идемпотентность и зачем она нужна?"

    Сильный ответ: Идемпотентность — свойство операции, при котором многократное выполнение с одними параметрами даёт тот же результат, что и однократное. В распределённых системах сети ненадёжны: клиент может не получить ответ из-за таймаута и не знать, выполнилась ли операция. Идемпотентные операции можно безопасно повторять. GET, PUT, DELETE — идемпотентны. POST — нет. Для POST используется паттерн Idempotency Key: клиент генерирует UUID, сервер сохраняет результат и при повторном запросе с тем же ключом возвращает кэшированный результат. Stripe, Stripe и большинство платёжных API используют этот паттерн.

    "Чем 401 отличается от 403?"

    Сильный ответ: 401 Unauthorized — несмотря на название, это про аутентификацию: клиент не идентифицирован. Ответ должен содержать заголовок WWW-Authenticate с указанием схемы аутентификации. 403 Forbidden — про авторизацию: клиент идентифицирован, но не имеет прав на ресурс. Повторный запрос с теми же credentials не поможет. Иногда сервер намеренно возвращает 404 вместо 403, чтобы не раскрывать существование ресурса (security through obscurity).

    "Как работает CORS и зачем он нужен?"

    Сильный ответ: CORS — механизм браузера, реализующий политику одного источника (same-origin policy). Браузер блокирует запросы к другому origin (протокол + домен + порт), если сервер явно не разрешает их. CORS — защита пользователя, не сервера: прямые запросы через curl не ограничиваются. Для "сложных" запросов (PUT, DELETE, кастомные заголовки) браузер сначала отправляет preflight OPTIONS-запрос. Сервер отвечает заголовками Access-Control-Allow-Origin, Access-Control-Allow-Methods и т.д. Критическая ошибка: Access-Control-Allow-Origin: * несовместим с Access-Control-Allow-Credentials: true.

    Блок 2: Теоретические вопросы Senior-уровня

    "Как бы вы реализовали rate limiting в распределённой системе?"

    Сильный ответ: В распределённой системе нельзя хранить счётчики локально — запросы приходят на разные серверы. Нужно централизованное хранилище. Redis — стандартный выбор: атомарные операции INCR и EXPIRE, высокая производительность.

    Алгоритм: Token Bucket через Redis с Lua-скриптом для атомарности:

    Проблема: Redis — единая точка отказа. Решение: Redis Cluster или алгоритм sliding window с несколькими Redis-нодами (алгоритм GCRA). Альтернатива: локальный rate limiting с синхронизацией через Redis каждые N секунд (eventual consistency для rate limiting).

    "Как обеспечить консистентность данных при использовании CQRS?"

    Сильный ответ: CQRS предполагает eventual consistency между command и query сторонами. Стратегии:

  • Optimistic UI: клиент обновляет UI немедленно, не дожидаясь синхронизации read-модели
  • Version tokens: команда возвращает версию, клиент передаёт её в следующий query — сервер ждёт, пока read-модель достигнет этой версии
  • Read-your-writes: после записи читаем из command-хранилища (с таймаутом переключаемся на read-хранилище)
  • Sticky sessions: запросы одного пользователя всегда идут на один read-replica
  • Важно: не пытайся сделать CQRS синхронным — это убивает все преимущества. Проектируй UI так, чтобы пользователь не замечал задержки (optimistic updates, skeleton screens).

    "Как спроектировать API для долгих операций?"

    Сильный ответ: Долгие операции (генерация отчёта, обработка видео, массовый импорт) не должны блокировать HTTP-соединение. Паттерн: асинхронная обработка с polling или webhook.

    Альтернатива polling: webhook (клиент регистрирует URL, сервер вызывает его при завершении) или SSE/WebSocket для real-time прогресса.

    Блок 3: System Design вопросы

    "Спроектируйте API для системы лайков в социальной сети"

    Это типичный system design вопрос. Правильный подход — задавать уточняющие вопросы:

  • Сколько пользователей? (влияет на масштаб)
  • Нужно ли показывать, кто именно лайкнул? (влияет на хранилище)
  • Нужен ли real-time счётчик? (влияет на архитектуру)
  • Базовый API:

    Проблемы масштаба:

    При 100M пользователях и 1B лайков в день прямое обновление счётчика в PostgreSQL создаст hotspot. Решения:

  • Счётчик в Redis: INCR post:42:likes_count — атомарно, быстро. Периодически синхронизируем в PostgreSQL.
  • Sharding: разбиваем лайки по post_id % N шардов
  • Approximate counting: HyperLogLog в Redis для приблизительного подсчёта уникальных пользователей (погрешность 0.81%)
  • Write-behind: лайки сначала в Redis, батчами записываем в PostgreSQL
  • "Как версионировать API без даунтайма?"

    Сильный ответ: Blue-green deployment + постепенная миграция:

  • Деплоим v2 рядом с v1 (оба работают)
  • Новые клиенты используют v2, старые — v1
  • Добавляем заголовок Deprecation к v1-ответам
  • Мониторим использование v1 (метрики по версиям)
  • Когда трафик v1 падает до нуля — отключаем
  • Для внутренних API: contract testing (Pact) гарантирует, что изменения не ломают потребителей. Тесты запускаются в CI перед деплоем.

    Блок 4: Разбор реальных ошибок

    Ошибка: "Мы используем POST для всего, потому что так проще"

    На собеседовании это красный флаг. Правильный ответ: POST для всего ломает кэширование (GET-запросы кэшируются браузером и CDN), нарушает идемпотентность (нельзя безопасно повторять), усложняет мониторинг (все запросы выглядят одинаково), нарушает принцип наименьшего удивления для других разработчиков.

    Ошибка: "Мы возвращаем 200 OK с полем error в теле"

    Это антипаттерн, который ломает: HTTP-кэширование (200 кэшируется), мониторинг (алерты на 5xx не сработают), клиентские библиотеки (они проверяют статус-код), load balancer health checks.

    Ошибка: "Мы храним JWT в localStorage"

    localStorage уязвим к XSS. Если на странице есть XSS-уязвимость, атакующий может прочитать токен. Правильно: httpOnly cookie (недоступна из JavaScript) + CSRF-защита (SameSite=Strict или CSRF-токен).

    Блок 5: Вопросы, которые стоит задать интервьюеру

    Хороший кандидат задаёт вопросы — это демонстрирует глубину мышления:

  • "Как вы сейчас обрабатываете breaking changes в API? Есть ли процесс deprecation?"
  • "Как устроен мониторинг API? Какие метрики отслеживаете?"
  • "Как команда принимает решения о выборе технологии (REST vs GraphQL vs gRPC)?"
  • "Какие самые сложные проблемы с API вы решали в последнее время?"
  • "Как организован процесс code review для изменений в API?"
  • Чеклист подготовки

    Middle-уровень — должен уверенно объяснить:

  • Все HTTP-методы, их семантику, идемпотентность
  • Коды ответов и когда какой использовать
  • Аутентификация vs авторизация, JWT, OAuth 2.0 flows
  • CORS: что это, как работает, типичные ошибки
  • Пагинация: offset vs cursor, когда что выбирать
  • Версионирование: подходы и breaking changes
  • Базовое кэширование: Cache-Control, ETag
  • Senior-уровень — должен уверенно объяснить и обосновать:

  • Trade-off'ы REST vs GraphQL vs gRPC
  • Rate limiting: алгоритмы, распределённая реализация
  • CQRS, Event Sourcing, Saga pattern
  • API Gateway vs BFF: когда что использовать
  • Eventual consistency: стратегии работы с клиентом
  • Security: OWASP API Top 10, BOLA, mass assignment
  • Observability: RED-метрики, distributed tracing
  • Проектирование API для долгих операций
  • Idempotency Key: реализация и edge cases
  • > Лучший способ подготовиться к вопросам о trade-off'ах — это не заучивать ответы, а понять, почему каждое решение существует и какую проблему оно решает. > > enigmai.ru

    !Чеклист подготовки к собеседованию: темы Middle и Senior уровней

    2. REST Maturity Model и уровни зрелости по Ричардсону

    REST Maturity Model и уровни зрелости по Ричардсону

    Модель зрелости Ричардсона (Richardson Maturity Model, RMM) — это практический инструмент оценки того, насколько API соответствует принципам REST. Леонард Ричардсон предложил её в 2008 году, разбив путь к "настоящему" REST на четыре уровня. Модель полезна не как догма, а как карта: она показывает, где находится ваш API сейчас и что нужно изменить, чтобы двигаться дальше.

    > The Richardson Maturity Model is a way to grade your API according to the constraints of REST. > > Martin Fowler — Richardson Maturity Model

    Уровень 0: Болото POX

    Уровень 0 (The Swamp of POX, Plain Old XML/JSON) — это использование HTTP исключительно как транспортного протокола. Вся логика передаётся в теле запроса, а HTTP-методы и URI не несут семантики.

    Типичный пример — SOAP-сервисы или самодельные RPC-API:

    Один эндпоинт, один метод (обычно POST), всё различается только телом запроса. Проблемы очевидны: невозможно кэшировать (кэш не знает, что getUser безопасен), нельзя использовать стандартные HTTP-инструменты, нет единообразия.

    Реальный пример из практики: многие внутренние корпоративные API, написанные в 2000-х годах, до сих пор работают на уровне 0. Они функциональны, но их сложно масштабировать и интегрировать с современными инструментами.

    Уровень 1: Ресурсы

    На уровне 1 появляются отдельные URI для каждого ресурса. Это уже большой шаг вперёд: вместо одного /api появляются /users, /orders, /products. Но HTTP-методы всё ещё используются неправильно — как правило, только POST или GET для всего.

    Что улучшилось: каждый ресурс имеет свой адрес, можно строить иерархии (/users/42/orders). Что осталось плохим: семантика HTTP-методов игнорируется, кэширование по-прежнему невозможно, идемпотентность не гарантируется.

    Многие API, которые разработчики называют "REST", на самом деле находятся на уровне 1. Это не катастрофа, но важно понимать ограничения.

    Уровень 2: HTTP-глаголы

    Уровень 2 — это то, что большинство разработчиков подразумевают под "REST API". Здесь правильно используются HTTP-методы: GET для чтения, POST для создания, PUT/PATCH для обновления, DELETE для удаления. Коды ответов тоже используются корректно.

    На этом уровне становятся возможными:

  • Кэширование GET-запросов на уровне браузера, CDN и прокси
  • Идемпотентность DELETE и PUT — клиент может безопасно повторять запросы
  • Мониторинг — инструменты понимают семантику методов и кодов
  • Стандартные клиентские библиотеки — работают корректно без дополнительной настройки
  • Подавляющее большинство современных публичных API (Stripe, Twilio, GitHub v3) работают на уровне 2. Это практически достаточный уровень для большинства задач.

    Уровень 3: Гипермедиа (HATEOAS)

    Уровень 3 — полноценный REST по Филдингу. Ответы содержат ссылки (hypermedia controls), которые описывают доступные действия. Клиент не должен знать структуру API заранее — он "открывает" её через ответы сервера.

    Обратите внимание: ссылка deactivate появляется только потому, что пользователь активен. Если бы статус был inactive, вместо неё появилась бы ссылка activate. Это и есть "гипермедиа как движок состояния приложения" — клиент видит только те действия, которые доступны в текущем состоянии.

    Форматы гипермедиа

    Существует несколько стандартизированных форматов для реализации HATEOAS:

    | Формат | Content-Type | Особенности | |---|---|---| | HAL | application/hal+json | Простой, широко поддерживается | | JSON:API | application/vnd.api+json | Строгая спецификация, включает пагинацию | | Siren | application/vnd.siren+json | Поддерживает описание форм и действий | | JSON-LD | application/ld+json | Семантическая веб-ориентация | | Collection+JSON | application/vnd.collection+json | Фокус на коллекциях |

    HAL (Hypertext Application Language) — наиболее распространённый выбор для новых API:

    _embedded позволяет включать связанные ресурсы прямо в ответ, избегая дополнительных запросов.

    Практическое применение модели

    На собеседовании вопрос о RMM часто звучит так: "На каком уровне зрелости находится ваш API и почему вы выбрали именно этот уровень?" Правильный ответ демонстрирует понимание trade-off'ов, а не слепое следование теории.

    Когда уровень 2 достаточен:

  • Публичные API с внешними клиентами, которые используют SDK
  • API с хорошей документацией (OpenAPI/Swagger)
  • Команды без опыта работы с HATEOAS
  • Большинство микросервисных взаимодействий
  • Когда уровень 3 оправдан:

  • API с динамически изменяющимися доступными действиями (workflow-системы, конечные автоматы)
  • Системы, где клиент не должен знать бизнес-правила (например, что кнопка "отменить" доступна только для заказов в статусе "pending")
  • Долгоживущие API, где нужна максимальная развязка между клиентом и сервером
  • Реальный кейс: система управления заказами в e-commerce. Заказ проходит через статусы: pending → confirmed → shipped → delivered. На каждом этапе доступны разные действия: отменить можно только pending и confirmed, вернуть — только delivered. С HATEOAS клиент просто смотрит на _links и показывает только доступные кнопки. Без HATEOAS клиент должен сам знать бизнес-правила и дублировать их — это нарушение принципа единственной ответственности.

    !Уровни зрелости REST по Ричардсону: от уровня 0 до уровня 3 с примерами

    Частые заблуждения о RMM

    Заблуждение 1: "Уровень 3 — всегда лучше". На практике HATEOAS добавляет сложность клиентскому коду и требует дополнительной работы на сервере. Если клиент — мобильное приложение с жёстко заданной логикой, HATEOAS не даёт реальных преимуществ.

    Заблуждение 2: "Уровень 2 — это настоящий REST". Рой Филдинг неоднократно подчёркивал, что без HATEOAS API не является REST. Но это академическая позиция; индустрия приняла уровень 2 как "достаточно хороший REST".

    Заблуждение 3: "RMM — это линейный прогресс, нужно всегда двигаться вверх". Модель — инструмент анализа, а не обязательный путь. Можно осознанно остановиться на уровне 2 и это будет правильным архитектурным решением.

    Заблуждение 4: "Уровень 0 — это всегда плохо". gRPC и GraphQL технически находятся на уровне 0 по RMM (один эндпоинт, POST), но это не делает их плохими технологиями. RMM применима только к REST-стилю.

    3. Версионирование API, обработка ошибок и идемпотентность

    Версионирование API, обработка ошибок и идемпотентность

    Стратегии версионирования API

    Версионирование (versioning) — это механизм, позволяющий вносить несовместимые изменения в API, не ломая существующих клиентов. Вопрос не в том, нужно ли версионировать, а в том, как это делать правильно.

    Существует четыре основных подхода, каждый со своими trade-off'ами.

    URI-версионирование

    Самый распространённый и наиболее очевидный способ:

    Плюсы: Очевидность — версия видна в URL, легко тестировать в браузере, просто кэшировать, легко маршрутизировать на уровне API Gateway.

    Минусы: Нарушает принцип REST (URI должен идентифицировать ресурс, а не его версию), приводит к дублированию кода, клиенты вынуждены менять все URL при обновлении.

    Используют: Stripe (/v1/), Twilio, большинство публичных API.

    Версионирование через заголовок

    или через Accept заголовок (content negotiation):

    Плюсы: URI остаётся чистым, соответствует REST-принципам, гибкость в управлении версиями.

    Минусы: Сложнее тестировать (нельзя просто открыть в браузере), сложнее кэшировать (нужно учитывать заголовок в ключе кэша), менее очевидно для новых разработчиков.

    Используют: GitHub API (Accept: application/vnd.github.v3+json), Microsoft Graph API.

    Версионирование через query-параметр

    Плюсы: Простота, не меняет структуру URI, удобно для постепенного перехода.

    Минусы: Загрязняет query string, может конфликтовать с другими параметрами, сложнее кэшировать.

    Используют: Azure REST API (?api-version=2023-01-01).

    Версионирование через дату

    Stripe использует именно этот подход: каждый клиент фиксирует дату версии API, и Stripe гарантирует совместимость для этой даты. Новые возможности добавляются, но поведение для зафиксированной даты не меняется.

    Плюсы: Гранулярность, клиент точно знает, с какой версией работает.

    Минусы: Сложность поддержки множества "дат" на сервере.

    | Подход | Кэшируемость | REST-чистота | Удобство | Популярность | |---|---|---|---|---| | URI /v1/ | Отличная | Низкая | Высокое | Очень высокая | | Заголовок | Средняя | Высокая | Среднее | Средняя | | Query param | Средняя | Средняя | Высокое | Средняя | | Дата | Средняя | Высокая | Среднее | Низкая |

    Что считается breaking change

    Критически важно понимать, какие изменения требуют новой версии:

    Breaking changes (требуют версии):

  • Удаление поля из ответа
  • Переименование поля
  • Изменение типа поля (stringinteger)
  • Изменение семантики поля
  • Удаление эндпоинта
  • Изменение структуры URL
  • Добавление обязательного поля в запрос
  • Non-breaking changes (безопасны):

  • Добавление нового необязательного поля в ответ
  • Добавление нового эндпоинта
  • Добавление нового необязательного параметра запроса
  • Добавление нового значения в enum (осторожно — клиент должен обрабатывать неизвестные значения)
  • Обработка ошибок: структура и стандарты

    Хорошая обработка ошибок — это не просто правильный HTTP-код. Клиент должен получить достаточно информации, чтобы понять, что пошло не так, и что с этим делать.

    Структура тела ошибки

    Минимально необходимые поля:

    RFC 7807 (Problem Details for HTTP APIs) — стандарт для структурирования ошибок:

    RFC 7807 хорош тем, что стандартизирован и поддерживается многими фреймворками. type — URI, идентифицирующий тип ошибки (не обязательно реальная ссылка, но желательно). instance — URI конкретного вхождения ошибки.

    Коды ошибок: машиночитаемые vs человекочитаемые

    Поле message — для разработчика, не для конечного пользователя. Никогда не показывай message из API напрямую в UI. Для локализации используй машиночитаемый code:

    Клиент использует code для определения нужного текста на языке пользователя. message — для логов и отладки.

    Типичные ошибки в обработке ошибок

    Антипаттерн 1: Возвращать 200 OK с {"success": false} в теле. Ломает мониторинг, кэширование и клиентские библиотеки.

    Антипаттерн 2: Раскрывать внутренние детали в сообщении об ошибке:

    Это security-уязвимость и плохой UX одновременно.

    Антипаттерн 3: Использовать один код для всех ошибок клиента. 400 Bad Request для всего — это потеря информации. Используй 422 для семантических ошибок валидации, 409 для конфликтов, 404 для отсутствующих ресурсов.

    Антипаттерн 4: Не включать requestId. Без него невозможно найти конкретный запрос в логах при разборе инцидента.

    Идемпотентность: теория и практика

    Идемпотентность (idempotency) — свойство операции, при котором многократное выполнение с одними и теми же параметрами даёт тот же результат, что и однократное выполнение. Формально: операция идемпотентна, если .

    Это критически важно в распределённых системах, где сети ненадёжны. Клиент не всегда знает, дошёл ли запрос до сервера — получил ли он таймаут до или после обработки. Идемпотентные операции можно безопасно повторять.

    | Метод | Идемпотентен | Безопасен | Объяснение | |---|---|---|---| | GET | Да | Да | Не меняет состояние | | HEAD | Да | Да | Как GET, без тела | | OPTIONS | Да | Да | Только метаданные | | PUT | Да | Нет | Повторный PUT — тот же результат | | DELETE | Да | Нет | Повторное удаление — ресурс всё равно удалён | | POST | Нет | Нет | Каждый вызов создаёт новый ресурс | | PATCH | Нет* | Нет | Зависит от реализации |

    *PATCH может быть идемпотентным, если операция абсолютная ({"status": "active"}), и не идемпотентным, если относительная ({"increment_count": 1}).

    Ключ идемпотентности

    Для POST-запросов (создание ресурсов, платежи, отправка email) используется паттерн Idempotency Key:

    Сервер сохраняет ключ и результат операции. При повторном запросе с тем же ключом возвращает сохранённый результат, не выполняя операцию повторно. Ключ генерирует клиент (обычно UUID v4).

    Stripe использует именно этот механизм. Если платёж завис из-за сетевой ошибки, клиент повторяет запрос с тем же Idempotency-Key и получает либо результат первого успешного выполнения, либо ошибку — но никогда не создаётся два платежа.

    Реализация на сервере

    Важные детали реализации:

  • Ключ должен быть уникальным в контексте конкретного клиента (добавляй client_id к ключу)
  • Храни результат достаточно долго (Stripe хранит 24 часа)
  • Если запрос с тем же ключом пришёл, пока первый ещё обрабатывается — верни 409 Conflict или подожди
  • Проверяй, что тело запроса совпадает с первым (разные тела с одним ключом — ошибка клиента)
  • !Схема работы Idempotency Key: клиент повторяет запрос, сервер возвращает кэшированный результат

    Eventual consistency и идемпотентность

    В распределённых системах идемпотентность особенно важна из-за eventual consistency — состояния, когда данные в разных узлах системы временно расходятся, но в конечном счёте приходят к согласованности. Если операция записи не идемпотентна, повторная попытка при сетевой ошибке может привести к дублированию данных.

    Практический пример: пользователь нажимает "Оплатить" в мобильном приложении. Запрос уходит, но ответ не приходит (таймаут). Приложение не знает: платёж прошёл или нет? Без Idempotency-Key повторный запрос создаст второй платёж. С ключом — сервер вернёт результат первого.

    4. Авторизация и безопасность API: OAuth 2.0, JWT и API Keys

    Авторизация и безопасность API: OAuth 2.0, JWT и API Keys

    Аутентификация vs авторизация

    Прежде чем разбирать механизмы — важно зафиксировать различие, которое путают даже опытные разработчики. Аутентификация (authentication) — это подтверждение личности: "кто ты?". Авторизация (authorization) — это проверка прав: "что тебе разрешено?". HTTP-код 401 Unauthorized — про аутентификацию, 403 Forbidden — про авторизацию.

    API Keys: простота с ограничениями

    API Key — это секретная строка, которую клиент передаёт с каждым запросом для идентификации. Самый простой механизм аутентификации.

    Когда использовать: Server-to-server коммуникация, простые публичные API, когда не нужна гранулярная авторизация.

    Ограничения:

  • Нет встроенного механизма истечения срока
  • Нет информации о пользователе — только об "приложении"
  • При компрометации нужно отзывать и перевыпускать вручную
  • Нет стандарта — каждый API реализует по-своему
  • Лучшие практики для API Keys: используй префикс для идентификации типа (sk_live_ для production, sk_test_ для sandbox), храни только хэш ключа в базе данных (как пароль), логируй использование с привязкой к ключу, реализуй ротацию ключей.

    JWT: структура и механизм работы

    JWT (JSON Web Token) — это компактный, самодостаточный токен для передачи информации между сторонами в виде JSON-объекта. Стандарт описан в RFC 7519.

    JWT состоит из трёх частей, разделённых точками: header.payload.signature.

    Header (декодированный):

    Payload (декодированный):

    Signature вычисляется как:

    Стандартные поля (claims) в payload:

  • sub — subject, идентификатор пользователя
  • iss — issuer, кто выдал токен
  • aud — audience, для кого предназначен токен
  • exp — expiration time, время истечения (Unix timestamp)
  • iat — issued at, время выдачи
  • jti — JWT ID, уникальный идентификатор токена (для blacklist)
  • Алгоритмы подписи

    | Алгоритм | Тип | Ключ | Когда использовать | |---|---|---|---| | HS256 | Симметричный | Один секрет | Один сервис, простота | | RS256 | Асимметричный | Пара RSA ключей | Несколько сервисов, публичная верификация | | ES256 | Асимметричный | Пара ECDSA ключей | Как RS256, но компактнее |

    RS256 предпочтителен в микросервисной архитектуре: auth-сервис подписывает токен приватным ключом, остальные сервисы верифицируют публичным. Публичный ключ можно распространять свободно — он не позволяет создавать токены.

    Критические уязвимости JWT

    Уязвимость 1: алгоритм "none". Ранние реализации позволяли указать "alg": "none" — токен принимался без проверки подписи. Всегда явно указывай допустимые алгоритмы на сервере.

    Уязвимость 2: смешение RS256 и HS256. Атакующий меняет alg с RS256 на HS256 и подписывает токен публичным ключом сервера (который известен). Уязвимая библиотека верифицирует HS256-подпись публичным ключом как секретом — и принимает токен. Решение: всегда явно указывай ожидаемый алгоритм.

    Уязвимость 3: хранение в localStorage. JWT в localStorage уязвим к XSS-атакам. Предпочтительно хранить в httpOnly cookie.

    Уязвимость 4: отсутствие проверки exp. Некоторые библиотеки не проверяют истечение автоматически — нужно явно включать эту проверку.

    Access Token и Refresh Token

    Короткоживущий access token (15 минут — 1 час) используется для доступа к ресурсам. Долгоживущий refresh token (7–30 дней) используется только для получения нового access token.

    Refresh token rotation: при каждом обновлении выдаётся новый refresh token, старый инвалидируется. Если старый refresh token используется повторно — это признак компрометации, все токены пользователя отзываются.

    OAuth 2.0: делегированная авторизация

    OAuth 2.0 — это фреймворк авторизации, позволяющий приложению получить ограниченный доступ к ресурсам пользователя на другом сервисе без передачи пароля. Описан в RFC 6749.

    Ключевые роли:

  • Resource Owner — пользователь, владелец данных
  • Client — приложение, запрашивающее доступ
  • Authorization Server — сервер, выдающий токены (Google, GitHub, ваш auth-сервис)
  • Resource Server — сервер с защищёнными ресурсами (ваш API)
  • Четыре grant type

    Authorization Code Flow — для веб-приложений с серверной частью. Самый безопасный flow:

    PKCE (Proof Key for Code Exchange) — расширение для мобильных и SPA-приложений, где нельзя безопасно хранить client_secret. Клиент генерирует code_verifier (случайная строка) и code_challenge (SHA-256 от verifier), передаёт challenge при запросе кода, а verifier — при обмене кода на токен.

    Client Credentials Flow — для machine-to-machine коммуникации, без участия пользователя:

    Implicit Flow — устаревший, не рекомендуется. Токен возвращается прямо в URL-фрагменте, что небезопасно.

    Resource Owner Password Credentials — устаревший, не рекомендуется. Клиент получает логин/пароль пользователя напрямую.

    !Схема Authorization Code Flow с PKCE для OAuth 2.0

    OpenID Connect: аутентификация поверх OAuth 2.0

    OpenID Connect (OIDC) — это тонкий слой поверх OAuth 2.0, добавляющий аутентификацию. OAuth 2.0 отвечает на вопрос "что приложению разрешено делать?", OIDC — на вопрос "кто этот пользователь?".

    OIDC добавляет:

  • ID Token — JWT с информацией о пользователе (кто аутентифицировался, когда, через какой провайдер)
  • UserInfo Endpoint — эндпоинт для получения дополнительных данных о пользователе
  • Стандартные scopes: openid, profile, email, address, phone
  • Разница между ID Token и Access Token: ID Token — для клиента (содержит информацию о пользователе), Access Token — для Resource Server (содержит права доступа). Не используй ID Token для авторизации запросов к API.

    Scopes и принцип минимальных привилегий

    Scopes определяют, к каким ресурсам и действиям имеет доступ токен. Принцип минимальных привилегий (least privilege): запрашивай только те scopes, которые действительно нужны.

    На сервере при каждом запросе проверяй не только валидность токена, но и наличие нужного scope:

    5. Защита API: rate limiting, CORS и распространённые атаки

    Защита API: rate limiting, CORS и распространённые атаки

    Rate Limiting: алгоритмы и реализация

    Rate limiting (ограничение частоты запросов) защищает API от злоупотреблений, DDoS-атак и случайных ошибок клиентов (бесконечные циклы). Существует несколько алгоритмов с разными характеристиками.

    Fixed Window

    Самый простой алгоритм: считаем запросы в фиксированных временных окнах (например, 100 запросов в минуту с 12:00:00 до 12:01:00).

    Проблема: граничный эффект. Клиент может сделать 100 запросов в 12:00:59 и ещё 100 в 12:01:01 — итого 200 запросов за 2 секунды, не нарушив лимит формально.

    Sliding Window Log

    Храним временные метки каждого запроса. При новом запросе удаляем метки старше окна и проверяем количество оставшихся.

    Плюс: точный подсчёт без граничного эффекта. Минус: высокое потребление памяти при большом количестве запросов.

    Sliding Window Counter

    Гибрид: используем два счётчика (текущее и предыдущее окно) и вычисляем взвешенное значение:

    Где elapsed — время, прошедшее с начала текущего окна. Это приближение скользящего окна с O(1) памятью.

    Token Bucket

    Ведро с токенами (Token Bucket) — наиболее гибкий алгоритм. Ведро имеет максимальную ёмкость (например, 100 токенов). Токены добавляются с постоянной скоростью (например, 10 в секунду). Каждый запрос потребляет один токен. Если токенов нет — запрос отклоняется.

    Преимущество: позволяет burst — кратковременные всплески трафика в пределах ёмкости ведра. Это важно для реальных пользователей: человек может быстро кликнуть несколько раз, и это нормально.

    Leaky Bucket

    Запросы поступают в "дырявое ведро" и обрабатываются с постоянной скоростью. Если ведро переполнено — новые запросы отклоняются. В отличие от Token Bucket, не допускает burst — выходной поток всегда равномерный. Используется для сглаживания трафика к downstream-сервисам.

    HTTP-заголовки rate limiting

    Стандартные заголовки для информирования клиента:

    Retry-After — количество секунд до следующей попытки. Клиент должен уважать этот заголовок и реализовывать exponential backoff с jitter.

    Уровни rate limiting

    Rate limiting можно применять на разных уровнях:

    | Уровень | Ключ | Пример | |---|---|---| | IP-адрес | ratelimit:ip:192.168.1.1 | 1000 req/hour per IP | | API Key | ratelimit:key:sk_live_abc | 10000 req/hour per key | | Пользователь | ratelimit:user:42 | 500 req/hour per user | | Эндпоинт | ratelimit:user:42:/payments | 10 req/min per user per endpoint | | Глобальный | ratelimit:global | 1M req/hour total |

    CORS: механизм и настройка

    CORS (Cross-Origin Resource Sharing) — механизм браузера, контролирующий, какие запросы с одного origin могут получать ресурсы с другого. Origin — это комбинация протокола, домена и порта: https://app.example.com:443.

    Важно понять: CORS — это защита браузера, а не сервера. Прямые запросы через curl или Postman не ограничиваются CORS. Атакующий, который уже имеет доступ к машине пользователя, тоже не ограничен CORS.

    Простые и предварительные запросы

    Простые запросы (simple requests) браузер отправляет напрямую: методы GET, HEAD, POST с определёнными Content-Type (text/plain, application/x-www-form-urlencoded, multipart/form-data).

    Предварительный запрос (preflight) браузер отправляет перед "сложными" запросами (PUT, DELETE, JSON body, кастомные заголовки):

    Access-Control-Max-Age: 86400 кэширует результат preflight на 24 часа — браузер не будет повторять OPTIONS-запрос для тех же комбинаций метода и заголовков.

    Типичные ошибки конфигурации CORS

    Ошибка 1: Access-Control-Allow-Origin: * с Access-Control-Allow-Credentials: true. Это невалидная комбинация — браузер отклонит её. Для запросов с credentials нужно указывать конкретный origin.

    Ошибка 2: Разрешать все origins в production:

    Vary: Origin важен для кэшей — он сигнализирует, что ответ зависит от заголовка Origin.

    Ошибка 3: Не обрабатывать OPTIONS-запросы — они должны возвращать 204 No Content без аутентификации.

    Распространённые атаки на API

    Injection атаки

    SQL Injection через API — классика. Если параметр запроса попадает в SQL без экранирования:

    Защита: параметризованные запросы, ORM, валидация входных данных. Никогда не конкатенируй пользовательский ввод в SQL-строки.

    NoSQL Injection — аналог для MongoDB и других NoSQL:

    Это вернёт первого пользователя из базы. Защита: валидация типов входных данных, использование ODM.

    BOLA / IDOR

    BOLA (Broken Object Level Authorization) — самая распространённая уязвимость API по версии OWASP API Security Top 10. Также известна как IDOR (Insecure Direct Object Reference).

    Защита: всегда проверяй, что текущий пользователь имеет право на доступ к конкретному объекту:

    Mass Assignment

    Клиент передаёт поля, которые не должен иметь возможности изменять:

    Если сервер слепо применяет все поля из запроса к модели — атакующий может повысить свои привилегии. Защита: явно указывай разрешённые поля (allowlist), никогда не используй **kwargs или аналоги для прямого маппинга запроса на модель.

    Excessive Data Exposure

    API возвращает больше данных, чем нужно клиенту, полагаясь на то, что клиент сам отфильтрует:

    Защита: явно определяй, какие поля возвращаются в каждом ответе. Используй DTO (Data Transfer Object) или сериализаторы с явным списком полей.

    Server-Side Request Forgery (SSRF)

    Атакующий заставляет сервер делать запросы к внутренним ресурсам:

    169.254.169.254 — адрес metadata-сервиса AWS. Атакующий может получить IAM credentials. Защита: валидируй URL (только разрешённые домены), используй allowlist, блокируй запросы к приватным IP-диапазонам.

    !Карта угроз API: OWASP Top 10 уязвимостей и методы защиты

    Security Headers

    Дополнительные заголовки для защиты:

    Для API (не веб-страниц) наиболее важны X-Content-Type-Options: nosniff и Strict-Transport-Security. CSP актуален для API, возвращающих HTML.

    6. Производительность и масштабируемость REST API

    Производительность и масштабируемость REST API

    Стратегии кэширования

    Кэширование — самый эффективный способ улучшить производительность API. Правильно реализованное кэширование снижает нагрузку на базу данных на 80–95% для read-heavy систем.

    HTTP-кэширование

    Встроенный механизм HTTP-протокола, работающий на уровне браузеров, CDN и прокси-серверов.

    Cache-Control — основной заголовок управления кэшем:

    ETag (Entity Tag) — механизм условных запросов. Сервер возвращает хэш содержимого, клиент отправляет его при следующем запросе:

    304 Not Modified не содержит тела — это экономит трафик. Клиент использует закэшированные данные.

    Last-Modified — альтернатива ETag на основе времени:

    ETag предпочтительнее Last-Modified: он точнее (время может совпасть при быстрых изменениях) и работает корректно при кластерных развёртываниях.

    Application-level кэширование

    Redis — стандарт для кэширования на уровне приложения. Паттерны:

    Cache-Aside (Lazy Loading) — самый распространённый:

    Write-Through — запись одновременно в кэш и БД. Кэш всегда актуален, но каждая запись медленнее.

    Write-Behind (Write-Back) — запись сначала в кэш, потом асинхронно в БД. Быстро, но риск потери данных при падении кэша.

    Cache Stampede (thundering herd) — проблема: когда кэш истекает, множество запросов одновременно идут в БД. Решение — probabilistic early expiration или mutex lock:

    Пагинация: три подхода

    Пагинация необходима для любого эндпоинта, возвращающего коллекции. Возвращать все записи без ограничений — прямой путь к OOM и таймаутам.

    Offset-based пагинация

    Плюсы: Простота реализации, можно перейти на любую страницу.

    Минусы: При большом offset производительность падает — база данных всё равно читает и пропускает первые N записей. При OFFSET 1000000 запрос будет медленным даже с индексом. Также: если между запросами страниц добавляются/удаляются записи, пользователь может пропустить элементы или увидеть дубликаты.

    Cursor-based пагинация

    Курсор — это закодированный указатель на последний элемент предыдущей страницы (обычно base64 от JSON с ID или timestamp):

    Плюсы: Стабильная производительность при любом размере данных, нет проблем с вставками/удалениями между запросами.

    Минусы: Нельзя перейти на произвольную страницу, сложнее реализовать.

    Используют: Twitter, Facebook, GitHub (для больших коллекций).

    Keyset пагинация

    Расширение cursor-based для сортировки по нескольким полям:

    Курсор содержит значения всех полей сортировки. Это единственный корректный способ пагинации при сортировке по неуникальному полю.

    Throttling и bulk операции

    Throttling отличается от rate limiting: rate limiting ограничивает количество запросов, throttling — скорость обработки. Throttling применяется для защиты downstream-сервисов от перегрузки.

    Bulk операции позволяют клиенту выполнить несколько операций в одном запросе:

    207 Multi-Status — специальный код для ответов, где разные операции могут иметь разные статусы. Важно: bulk-операции должны быть атомарными (всё или ничего) или явно указывать, что частичный успех возможен.

    Оптимизация запросов к БД

    N+1 проблема — классическая ловушка производительности. Запрашиваем список пользователей (1 запрос), потом для каждого запрашиваем его заказы (N запросов):

    Sparse fieldsets — возвращай только запрошенные поля:

    Projection в MongoDB:

    Observability: метрики, трейсинг и логирование

    Observability (наблюдаемость) — способность понять внутреннее состояние системы по её внешним выходным данным. Три столпа: метрики, трейсинг, логирование.

    Ключевые метрики API

    RED-метрики (Rate, Errors, Duration) — минимальный набор для любого API:

  • Rate — количество запросов в секунду (RPS)
  • Errors — процент ошибочных ответов (4xx, 5xx)
  • Duration — время ответа (p50, p95, p99)
  • Почему важны перцентили, а не среднее: если 99% запросов выполняются за 10 мс, а 1% — за 10 секунд, среднее будет ~110 мс — вполне приемлемо. Но 1% пользователей ждут 10 секунд. p99 = 10 секунд — это проблема.

    Distributed Tracing

    В микросервисной архитектуре один запрос проходит через несколько сервисов. Distributed tracing позволяет отследить весь путь запроса.

    traceparent содержит: версию, trace ID (уникальный для всего запроса), span ID (уникальный для текущего сервиса), флаги.

    Инструменты: Jaeger, Zipkin, OpenTelemetry (стандарт де-факто для инструментации).

    Структурированное логирование

    Логи должны быть машиночитаемыми для эффективного поиска:

    Никогда не логируй: пароли, токены, номера карт, персональные данные (PII). Используй маскирование: "email": "a*@example.com".

    !Схема observability: метрики, трейсинг и логирование в микросервисной архитектуре

    7. Документирование API с помощью OpenAPI и Swagger

    Документирование API с помощью OpenAPI и Swagger

    OpenAPI Specification: структура и назначение

    OpenAPI Specification (OAS) — это стандарт описания REST API в машиночитаемом формате (YAML или JSON). Версия 3.x является актуальной; Swagger — это инструментарий вокруг OAS, а также историческое название спецификации версии 2.x. Сегодня "Swagger" часто используют как синоним OpenAPI, хотя технически это разные вещи.

    Ключевая ценность OAS: одна спецификация служит источником истины для документации, генерации клиентских SDK, mock-серверов, тестов и валидации запросов/ответов.

    Минимальная структура OpenAPI 3.x документа

    Схемы с наследованием

    OpenAPI поддерживает полиморфизм через oneOf, anyOf, allOf:

    discriminator помогает инструментам определить, какую схему использовать при десериализации.

    Contract-First vs Contract-Last

    Это фундаментальный выбор в подходе к разработке API.

    Contract-First (Design-First): сначала пишем спецификацию OpenAPI, потом реализуем. Спецификация — источник истины.

    Contract-Last (Code-First): сначала пишем код, потом генерируем спецификацию из аннотаций.

    | Критерий | Contract-First | Contract-Last | |---|---|---| | Скорость старта | Медленнее | Быстрее | | Качество API | Выше (обдуманный дизайн) | Зависит от разработчика | | Синхронизация | Всегда актуальна | Риск расхождения | | Параллельная разработка | Да (frontend/backend одновременно) | Нет | | Рефакторинг | Сложнее | Проще | | Подходит для | Публичных API, больших команд | Внутренних API, прототипов |

    Contract-First — предпочтительный подход для публичных API. Спецификация позволяет frontend-команде начать разработку параллельно с backend, используя mock-сервер.

    Генерация кода из спецификации

    Swagger UI и ReDoc

    Swagger UI — интерактивная документация, позволяющая тестировать API прямо в браузере. Интегрируется в большинство фреймворков:

    ReDoc — альтернативный рендерер, более удобный для чтения (трёхколоночный layout). Не поддерживает интерактивное тестирование, но лучше подходит для публичной документации.

    Расширения и best practices

    x-расширения

    OpenAPI позволяет добавлять кастомные поля с префиксом x-:

    Версионирование спецификации

    Храни спецификацию в Git рядом с кодом. Используй семантическое версионирование в info.version. При breaking changes — создавай новый файл (api-v2.yaml) или используй теги.

    Валидация запросов по спецификации

    Многие фреймворки поддерживают автоматическую валидацию входящих запросов по OpenAPI-схеме:

    validateResponses: true в разработке — мощный инструмент: если сервер возвращает ответ, не соответствующий спецификации, это сразу видно. В production отключай — это дополнительная нагрузка.

    !Экосистема OpenAPI: от спецификации к документации, SDK и тестам

    AsyncAPI для асинхронных API

    AsyncAPI — аналог OpenAPI для event-driven и асинхронных API (WebSocket, Kafka, AMQP). Структура похожа, но вместо paths используются channels:

    AsyncAPI рассматривается подробнее в статье об асинхронных API.

    8. Введение в GraphQL: схема, запросы и мутации

    Введение в GraphQL: схема, запросы и мутации

    Что такое GraphQL и зачем он нужен

    GraphQL — это язык запросов для API и среда выполнения этих запросов, разработанный Facebook в 2012 году и открытый в 2015-м. GraphQL решает конкретные проблемы REST, которые особенно болезненны для мобильных приложений и сложных клиентов.

    Проблема overfetching: REST-эндпоинт /users/42 возвращает полный объект пользователя — 20 полей. Мобильному приложению нужны только name и avatar. Остальные 18 полей — лишний трафик.

    Проблема underfetching: Для отображения профиля пользователя с его последними заказами и адресами доставки нужно сделать 3 запроса: /users/42, /users/42/orders, /users/42/addresses. Каждый запрос — отдельный round-trip.

    GraphQL решает обе проблемы: клиент запрашивает ровно те данные, которые нужны, и получает их за один запрос.

    Система типов GraphQL

    Схема (schema) — центральная концепция GraphQL. Она определяет все типы данных и операции, доступные в API. Схема — это контракт между клиентом и сервером.

    Скалярные типы

    Встроенные скаляры: Int, Float, String, Boolean, ID. Можно определять кастомные:

    Объектные типы

    ! означает non-null — поле гарантированно не будет null. [Order!]! — список не null, и каждый элемент не null. [Order] — список может быть null, и элементы могут быть null. Это важное различие для клиентского кода.

    Input типы

    Для аргументов мутаций используются специальные input типы (нельзя использовать обычные type):

    Интерфейсы и Union типы

    Запросы (Queries)

    Query — операция чтения данных. Клиент точно указывает, какие поля нужны:

    Аргументы и переменные

    Жёстко закодированные аргументы в запросе — плохая практика. Используй переменные:

    Aliases и Fragments

    Aliases позволяют запросить один тип несколько раз с разными аргументами:

    Fragments — переиспользуемые наборы полей:

    Inline Fragments для Union и Interface

    Мутации (Mutations)

    Mutation — операция изменения данных. По соглашению мутации выполняются последовательно (в отличие от запросов, которые могут выполняться параллельно).

    Мутация возвращает изменённый объект — это позволяет клиенту обновить локальный кэш без дополнительного запроса.

    Subscriptions

    Subscription — механизм real-time обновлений через WebSocket:

    Клиент подписывается и получает события каждый раз, когда статус заказа меняется. Сервер отправляет данные только при изменении — в отличие от polling.

    Обработка ошибок в GraphQL

    GraphQL всегда возвращает 200 OK (кроме сетевых ошибок). Ошибки передаются в поле errors:

    Важный нюанс: data может быть частично заполнен при частичных ошибках. Если запрос запрашивает user и orders, и user найден, а orders вернул ошибку — data.user будет заполнен, data.orders будет null, а в errors будет описание проблемы с orders.

    Это отличается от REST, где ошибка одного ресурса означает ошибку всего запроса. В GraphQL клиент должен проверять и data, и errors.

    !Сравнение REST и GraphQL: overfetching, underfetching и точные запросы

    Resolver-функции

    Resolver — функция, которая возвращает данные для конкретного поля схемы. Каждое поле в GraphQL имеет свой resolver:

    context — объект, доступный всем resolvers. Обычно содержит: аутентифицированного пользователя, data sources, логгер, request ID.

    9. Продвинутый GraphQL: batching, caching и проблема N+1

    Продвинутый GraphQL: batching, caching и проблема N+1

    Проблема N+1 в GraphQL

    Проблема N+1 в GraphQL возникает из-за того, что resolvers выполняются независимо для каждого объекта. Рассмотрим запрос:

    Если users вернул 100 пользователей, resolver User.orders вызовется 100 раз — итого 101 запрос к базе данных. При 1000 пользователях — 1001 запрос. Это катастрофа для производительности.

    Проблема усугубляется тем, что GraphQL-схема позволяет клиенту строить произвольно глубокие запросы. Клиент может запросить users → orders → items → product → category — и каждый уровень умножает количество запросов.

    DataLoader: решение проблемы N+1

    DataLoader — библиотека, разработанная Facebook, реализующая паттерн batching (пакетная загрузка) и caching (кэширование) для resolvers.

    Принцип работы DataLoader:

  • Resolver вызывает dataLoader.load(userId) — это не выполняет запрос немедленно
  • DataLoader накапливает все вызовы load() в течение одного тика event loop
  • В конце тика вызывает batch-функцию с массивом всех ID
  • Batch-функция делает один запрос к БД с WHERE id IN (1, 2, 3, ...)
  • DataLoader распределяет результаты обратно по ожидающим resolvers
  • Результат: вместо 101 запроса при 100 пользователях — 2 запроса (один для users, один batch для orders).

    DataLoader с кэшированием

    DataLoader автоматически кэширует результаты в рамках одного запроса:

    DataLoader с кастомными ключами

    Иногда нужна пакетная загрузка по составному ключу:

    !Схема работы DataLoader: batching запросов к базе данных