Нестандартные вопросы про RESTful API cover

Нестандартные вопросы про RESTful API

Опубликовано: 09 июня, 2026, Обновлено: 09 июня, 2026

Наборы по теме

REST API: архитектура, практика и собеседования
Глубокий набор карточек по REST API: архитектура, контракты, безопасность, масштабирование, ошибки и production-сценарии для middle, senior и system design.
122

Книги по теме

Обложка книги "RESTful Web API паттерны и практики. Связывание и оркестрация микросервисов и распределение данных"
Реклама. ООО ЛИТРЕС, ИНН 7719571260, erid: 2VfnxyNkZrY

REST API часто обсуждают через базовые темы: HTTP-методы, статус-коды, URL-структуру, CRUD и версионирование. Но на практике сложность REST начинается не там. Настоящие проблемы появляются, когда API используют разные клиенты, контракт начинает жить годами, бизнес-логика усложняется, данные агрегируются, операции становятся массовыми, а внешние интеграции требуют стабильности и предсказуемости.

Эта статья разбирает не самые стандартные, но очень важные вопросы проектирования REST API: контракты, валидацию, обратную совместимость, bulk operations, webhooks, защиту от атак, поиск, комментарии и производительность.

Полный список вопросов можно изучить с помощью набора REST API: архитектура, практика и собеседования


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

Контракт REST API — это формальное соглашение между сервером и клиентом о том, какие данные клиент может отправлять, какие ответы он получит, какие ошибки возможны и какие правила должны соблюдаться. На практике контракт важнее самой реализации, потому что именно от него зависят стабильность интеграций, скорость разработки клиентов и возможность безопасно развивать API.

Контракт обычно включает несколько частей: структуру URL, HTTP-методы, request schema, response schema, статус-коды, формат ошибок, правила авторизации, пагинацию, фильтрацию и ограничения. Например, endpoint POST /orders может принимать тело запроса с items, deliveryAddress, paymentMethodId, а возвращать созданный заказ с id, status, totalAmount, createdAt.

Request schema должна описывать, какие поля принимает API, какие из них обязательные, какие опциональные, какие типы данных допустимы и какие ограничения применяются. Например, поле email должно быть строкой в формате email, quantity — целым числом больше нуля, items — непустым массивом. Важно явно разделять обязательные и необязательные поля. Если поле действительно необходимо для выполнения операции, оно должно быть required. Если сервер может сам вывести значение или использовать дефолт, поле лучше оставить опциональным.

Response schema не менее важна. Клиенты строят UI, бизнес-логику и интеграции на основе ответа API. Поэтому ответ должен быть стабильным, предсказуемым и документированным. Если API возвращает заказ, клиент должен понимать, какие поля всегда присутствуют, какие могут быть null, какие могут отсутствовать, а какие являются вычисляемыми.

Главная проблема при изменении контракта — обратная совместимость. В REST API нельзя без последствий просто удалить поле, переименовать его, изменить тип или начать возвращать другую структуру. Даже если frontend уже обновлён, могут существовать мобильные приложения старых версий, внешние партнёры, скрипты, BI-интеграции или внутренние сервисы, которые всё ещё используют старый контракт.

Безопасными обычно считаются additive changes: добавление нового необязательного поля в request, добавление нового поля в response, добавление нового значения enum при условии, что клиенты умеют обрабатывать неизвестные значения. Опасными изменениями являются удаление поля, изменение типа, изменение смысла поля, изменение обязательности, изменение формата даты, переименование поля, изменение структуры вложенных объектов.

Например, если раньше API возвращал:

а затем поле total было заменено на объект:

это breaking change. Даже если новая структура логичнее, старые клиенты сломаются.

Правильный подход — сначала добавить новое поле, например totalAmount и currency, оставить старое поле total, объявить его deprecated, дать клиентам время на миграцию, собрать метрики использования и только потом удалять поле в новой версии API.

Хороший контракт должен быть не просто описанием текущей реализации, а долгосрочным публичным интерфейсом системы. Его нужно проектировать так, чтобы он выдерживал изменение бизнес-логики, рост числа клиентов и постепенную эволюцию продукта.


Валидация входящих данных

Request validation — это проверка входящих данных до выполнения бизнес-операции. Её цель — защитить систему от некорректных данных, сделать ошибки понятными для клиента и не допустить попадания мусора в бизнес-логику.

Валидацию полезно разделять на несколько уровней.

Первый уровень — синтаксическая и структурная валидация. Здесь проверяется, что request body является валидным JSON, содержит нужные поля, поля имеют правильные типы, строки не превышают лимиты, числа входят в допустимый диапазон, массивы не пустые, даты имеют правильный формат.

Например, для создания товара можно проверить:

Здесь title должен быть строкой, price — положительным числом, categoryId — числом или UUID в зависимости от модели данных.

Если клиент отправил price: "abc" или не передал title, это ошибка формата запроса. Обычно такая ошибка возвращается со статусом 400 Bad Request или 422 Unprocessable Entity, в зависимости от принятого стиля API.

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

Такие ошибки уже не связаны с неправильным JSON. Запрос может быть технически корректным, но недопустимым с точки зрения бизнес-правил.

Важно различать синтаксическую и доменную ошибку. Например:

Это синтаксическая ошибка: тип поля неверный.

А вот:

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

Для клиента это различие важно. Синтаксическая ошибка обычно означает ошибку в коде клиента или форме ввода. Доменная ошибка часто должна быть показана пользователю как бизнес-сообщение: “Недостаточно товара на складе”, “Промокод истёк”, “Заказ уже оплачен”.

Хороший формат ошибок должен быть машинно-читаемым и человеко-понятным. Например:

Для доменной ошибки формат может быть таким:

Request validation не должна быть хаотично размазана по контроллерам. Лучше выделять слой DTO/schema validation для проверки формы запроса и отдельный доменный слой для проверки бизнес-правил. Это делает систему понятнее, тестируемее и безопаснее при изменениях.


Валидация ответов API

Response validation — это проверка того, что API действительно возвращает ответ, соответствующий заявленному контракту. Часто команды уделяют много внимания валидации входящих данных, но почти не проверяют исходящие ответы. Это опасно, потому что breaking changes чаще всего появляются случайно: разработчик переименовал поле, изменил тип, убрал null, изменил enum или структуру вложенного объекта.

Response validation помогает обнаружить такие проблемы до того, как они попадут к клиентам.

Например, если контракт говорит, что GET /users/{id} возвращает:

то тесты или runtime validation могут проверить, что API действительно возвращает все эти поля в нужном формате.

Response validation можно делать на нескольких уровнях.

Первый вариант — контрактные тесты. Они запускаются в CI и проверяют, что ответы endpoint’ов соответствуют OpenAPI-схеме или другой спецификации. Это хороший способ защититься от случайных breaking changes.

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

Третий вариант — runtime response validation. Сервер на этапе разработки или в staging окружении проверяет собственные ответы перед отправкой клиенту. В production такой подход нужно использовать осторожно, потому что он добавляет накладные расходы и может сам стать источником ошибок. Но для критичных API или внутренних платформ это может быть оправдано.

Response validation особенно полезна при работе с сериализацией. Например, ORM-модель может содержать внутренние поля: passwordHash, internalNotes, deletedAt, adminFlags. Если не контролировать response schema, такие поля могут случайно попасть в API-ответ. Это уже не только breaking change, но и проблема безопасности.

Также response validation помогает при рефакторинге. Если команда меняет базу данных, ORM, слой DTO или формат хранения, контрактные тесты показывают, не сломался ли внешний API.

Влияние на интеграционные тесты значительное. Если тесты проверяют только statusCode === 200, они почти бесполезны для защиты контракта. Хорошие интеграционные тесты должны проверять ключевые поля, типы, наличие обязательных данных, допустимые значения enum, структуру ошибок и поведение при edge cases.

Однако не стоит превращать тесты в копию всей реализации. Если тесты слишком жёстко проверяют каждую мелочь, они будут ломаться при любом безопасном изменении. Например, добавление нового поля в response не должно ломать тесты. Поэтому проверки должны быть строгими к обязательному контракту, но гибкими к additive changes.


Контракты для разных клиентов

Одна из сложных задач REST API — поддержка разных типов клиентов. Web-приложение, мобильное приложение и внешняя интеграция могут использовать одни и те же бизнес-данные, но иметь разные потребности.

Web-клиент часто может быстро обновляться, использовать более тяжёлые ответы, активнее работать с фильтрами и состоянием. Мобильный клиент ограничен версией приложения, сетью, размером ответа, батареей и backward compatibility. Внешние интеграции требуют стабильности, документации, предсказуемости и долгого жизненного цикла контракта.

Есть два основных подхода.

Первый подход — универсальный API. Например, GET /products/{id} возвращает один стандартный формат товара для всех клиентов. Это проще поддерживать, документировать и тестировать. Меньше endpoint’ов, меньше дублирования, проще консистентность.

Но универсальный API часто приводит к компромиссам. Одному клиенту нужны дополнительные поля, другому они не нужны. Web хочет получить подробную карточку товара, mobile хочет компактный ответ, external partner хочет стабильную структуру без UI-специфичных данных. В итоге универсальный endpoint может стать слишком большим, тяжёлым и неудобным для всех.

Второй подход — client-specific endpoints или BFF-подход. Например:

GET /web/products/{id}
GET /mobile/products/{id}
GET /partners/products/{id}

Или отдельный Backend for Frontend для web и mobile.

Такой подход позволяет оптимизировать ответы под конкретный клиент. Mobile может получить только нужные поля, web — агрегированные данные для страницы, external integrations — стабильный публичный контракт. Это удобно, но увеличивает количество endpoint’ов, усложняет поддержку и создаёт риск расхождения бизнес-логики.

На практике часто используют гибридный подход. Базовые ресурсы остаются универсальными, а client-specific endpoints создаются только там, где есть реальная необходимость: сложные экраны, высокая нагрузка, разные требования к данным, оптимизация мобильного трафика или внешние партнёрские API.

Например, для товара можно иметь универсальный endpoint:

GET /products/{id}

и отдельный endpoint для карточки товара в мобильном приложении:

GET /mobile/product-page/{id}

Второй endpoint может вернуть не только товар, но и рейтинг, наличие, похожие товары, промо-блоки и настройки отображения.

Важно не смешивать публичный API и внутренний API для frontend. Публичный API для внешних клиентов должен быть стабильным, минимальным и хорошо документированным. API для собственного UI может быть более гибким и чаще меняться.

Главный trade-off такой: универсальный API проще поддерживать, но он может быть неэффективен для конкретных клиентов. Client-specific API удобнее и быстрее для клиентов, но сложнее в сопровождении. Хорошее проектирование начинается с понимания жизненного цикла клиентов и стоимости изменений для каждого из них.


Агрегированные ресурсы

Агрегированные ресурсы нужны, когда клиенту недостаточно одной сущности. Например, страница заказа может требовать данные заказа, пользователя, товаров, оплаты, доставки и итоговых сумм. Если клиент будет получать всё отдельными запросами, появится много round-trip’ов, сложная логика на клиенте и риск N+1 проблем.

Агрегированный ресурс — это API-ответ, который собирает данные из нескольких источников и отдаёт их как одну удобную структуру.

Например:

Здесь summary содержит вычисляемые поля. Они удобны для клиента, потому что клиенту не нужно самостоятельно считать итоговую сумму, количество товаров или скидки. Но такие поля создают ответственность на сервере: вычисления должны быть корректными, повторяемыми и согласованными с бизнес-логикой.

Денормализация ответа — нормальная практика для REST API. В базе данных данные могут быть разложены по таблицам, но API не обязан повторять структуру базы. Ответ должен быть удобен для потребителя. Например, в заказе можно вернуть productTitle, даже если название товара хранится в таблице products.

Но денормализация несёт риск рассинхронизации. Если данные копируются или кэшируются, может возникнуть ситуация, когда в одном месте название товара уже обновилось, а в агрегированном ответе ещё старое значение. Поэтому нужно понимать, какие данные должны быть strictly consistent, а какие допускают eventual consistency.

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

При проектировании агрегированных ресурсов важно ответить на несколько вопросов:

  • является ли агрегированный ответ read model или частью core domain;
  • можно ли кэшировать этот ответ;
  • какие поля вычисляются на лету, а какие заранее;
  • какие данные критичны к консистентности;
  • как обновляются денормализованные поля;
  • что будет при частичной недоступности одного из источников данных.

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

Хороший REST API не должен заставлять клиента собирать сложный экран из десятков мелких запросов. Но и сервер не должен превращать каждый endpoint в тяжёлый универсальный комбайн. Агрегация должна проектироваться под реальные сценарии использования.


Массовые операции

Bulk operations позволяют выполнить массовое действие за один запрос: создать несколько объектов, обновить несколько записей, удалить группу сущностей или применить одно действие к набору ресурсов. Это важно для производительности, удобства клиента и снижения количества HTTP-запросов.

Примеры bulk endpoints:

POST /products/bulk
PATCH /products/bulk
DELETE /products/bulk
POST /orders/bulk-cancel

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

Главная архитектурная проблема bulk operations — частичный успех. Что делать, если из 100 элементов 95 обработались успешно, а 5 завершились ошибкой?

Есть два подхода.

Первый подход — atomic bulk operation. Вся операция либо полностью успешна, либо полностью отменяется. Это удобно с точки зрения консистентности. Если один элемент невалиден, не создаётся ничего. Такой подход хорошо работает, если операция выполняется внутри одной транзакции и количество элементов ограничено.

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

Второй подход — partial success. Каждый элемент обрабатывается независимо, а API возвращает результат по каждому элементу. Например:

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

Но partial success сложнее. Нужно проектировать формат ответа, idempotency, повторные попытки, логи, аудит и состояние операции. Также нужно решить, какой HTTP-статус возвращать. Часто для полной успешной обработки используют 200 OK или 201 Created, для полностью невалидного запроса — 400 или 422, а для частичного результата — 207 Multi-Status или 200 OK с подробным телом ответа. На практике 200 OK с явным массивом результатов часто проще для клиентов, чем редкий статус 207.

Для больших bulk operations лучше использовать асинхронную модель:

POST /imports
GET /imports/{id}

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

При проектировании bulk operations важно ограничивать размер пачки. Например, максимум 100 или 1000 элементов за запрос. Без лимитов bulk endpoint легко превращается в источник перегрузки API и базы данных.

Также нужно учитывать idempotency. Если клиент отправил bulk-запрос, получил timeout и повторил запрос, сервер не должен случайно создать дубликаты. Для этого используют idempotency keys, внешние идентификаторы или уникальные ключи на уровне элементов.

Bulk operations должны проектироваться не как “обычный CRUD, только массивом”, а как отдельный сценарий с собственными правилами ошибок, повторов, транзакционности и лимитов.


Обратная совместимость

Backward compatibility — это способность API развиваться, не ломая существующих клиентов. Для REST API это особенно важно, потому что клиенты могут жить долго: мобильные приложения не обновляются мгновенно, внешние партнёры могут менять интеграции месяцами, а внутренние системы иногда используют API годами без активной поддержки.

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

Самый безопасный тип изменений — additive changes. К ним относятся:

  • добавление нового поля в response;
  • добавление нового необязательного поля в request;
  • добавление нового endpoint’а;
  • добавление новых фильтров;
  • расширение ответа дополнительной информацией;
  • добавление новых значений enum при условии, что клиенты готовы к неизвестным значениям.

Но даже additive changes иногда могут ломать клиентов. Например, строго типизированный клиент может падать при неизвестном enum. Или старый парсер может не ожидать новые поля. Поэтому хорошая документация должна прямо говорить клиентам: игнорируйте неизвестные поля и будьте готовы к расширению enum.

Breaking changes включают удаление поля, переименование поля, изменение типа, изменение обязательности, изменение структуры ответа, изменение смысла поля, изменение формата ошибки, изменение правил авторизации, изменение сортировки по умолчанию, если клиент от неё зависел.

Deprecation policy нужна для управляемого удаления старого поведения. Она должна отвечать на вопросы:

  • как объявляется устаревшее поле или endpoint;
  • сколько времени оно будет поддерживаться;
  • как клиенты узнают о deprecation;
  • где опубликована миграционная инструкция;
  • как команда отслеживает использование старого контракта;
  • что произойдёт после окончания migration window.

Например, можно объявить, что deprecated поля поддерживаются минимум 6 или 12 месяцев, а для external integrations — дольше. Для мобильных приложений окно миграции часто должно учитывать реальные версии приложения в App Store и Google Play.

Migration window — это период, в течение которого старый и новый контракт работают параллельно. Например, поле total объявляется deprecated, добавляются totalAmount и currency, клиенты переходят на новые поля, после чего старое поле удаляется только в следующей major version.

Проблема редко обновляемых клиентов особенно важна для mobile. Пользователь может годами не обновлять приложение. Если сервер резко изменит контракт, старое приложение перестанет работать. Поэтому для mobile API часто используют явное версионирование, feature flags, минимальную поддерживаемую версию приложения и мягкие механизмы отключения старых версий.

Backward compatibility — это не только технический вопрос. Это процесс. Нужны документация, changelog, метрики использования endpoint’ов, логирование client version, договорённости с партнёрами и дисциплина разработки. API, который используется многими клиентами, нельзя менять как внутреннюю функцию.


Защита от атак на авторизацию

Brute force и credential stuffing — это атаки на механизмы аутентификации. Brute force пытается подобрать пароль. Credential stuffing использует уже украденные пары логин/пароль из других сервисов и массово проверяет их на вашем API.

Защита должна быть многоуровневой. Один rate limit по IP не решает проблему, потому что атаки могут идти через ботнет, прокси или распределённые адреса.

Throttling ограничивает частоту запросов. Его можно применять по IP, по аккаунту, по email, по device fingerprint, по подсети, по client id. Например, можно ограничить количество попыток входа на один аккаунт до 5 за минуту и до 20 за час. Но важно не сделать защиту слишком простой для обхода. Если лимит только по IP, атакующий распределит запросы. Если лимит только по аккаунту, можно устроить denial-of-service, блокируя чужие аккаунты.

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

Captcha может помочь отличить человека от бота, но ухудшает пользовательский опыт. Поэтому не стоит показывать captcha всем пользователям всегда. Лучше включать её при подозрительном поведении: много ошибок, необычная страна, новый девайс, подозрительный IP, массовые попытки входа.

Risk-based checks позволяют оценивать контекст запроса. Например, система может учитывать географию, устройство, историю входов, скорость попыток, репутацию IP, совпадение с известными утечками, поведение пользователя. При низком риске вход проходит обычно. При среднем риске запрашивается MFA. При высоком — вход блокируется или требует дополнительной проверки.

Для credential stuffing особенно полезны:

  • MFA;
  • проверка паролей по базам известных утечек;
  • rate limits по аккаунту и IP;
  • detection массовых попыток;
  • уведомления о подозрительных входах;
  • защита endpoint’ов login, password reset, signup;
  • одинаковые сообщения об ошибке для неверного логина и пароля.

Важно не раскрывать, существует ли аккаунт. Сообщение “пользователь не найден” помогает атакующему собирать базу email. Лучше использовать нейтральное сообщение: “Неверный логин или пароль”.

Trade-off заключается в балансе безопасности и удобства. Слишком мягкая защита пропускает атаки. Слишком жёсткая раздражает реальных пользователей, блокирует легитимные входы и увеличивает нагрузку на поддержку. Поэтому защита должна быть адаптивной: обычным пользователям не мешать, подозрительных — замедлять и проверять.


Проблема N+1

N+1 проблема возникает, когда API сначала получает список сущностей, а затем для каждой сущности выполняет отдельный дополнительный запрос. Например, endpoint GET /orders возвращает 100 заказов, и для каждого заказа отдельно загружает пользователя. В результате получается 1 запрос за заказами и 100 запросов за пользователями.

На маленьких данных это может быть незаметно. На реальной нагрузке N+1 приводит к деградации производительности, росту latency и перегрузке базы данных.

Первый способ борьбы — eager loading. Если заранее известно, что вместе с заказами нужны пользователи и товары, можно загрузить их одним запросом через JOIN или несколькими оптимизированными запросами. Например, ORM может позволять указать include: ['customer', 'items'].

Но eager loading тоже нужно использовать осторожно. Если всегда загружать все связи, ответы станут тяжёлыми, а запросы к базе — дорогими.

Второй способ — batching. Вместо 100 запросов за пользователями можно собрать все userId, выполнить один запрос WHERE id IN (...), а затем сопоставить пользователей с заказами в памяти. Это часто эффективнее и проще контролируется.

Третий способ — dataloader-подход. Он популярен в GraphQL, но применим и в REST. Идея в том, чтобы в рамках одного request lifecycle собирать запросы на связанные сущности, группировать их и выполнять батчами. Также можно кэшировать результаты внутри запроса, чтобы один и тот же пользователь не загружался несколько раз.

В REST API часто используют параметры include или expand:

GET /orders?include=customer,items
GET /orders?expand=customer

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

GET /orders?include=customer,items.product.category.parent.children

Поэтому include/expand должны иметь ограничения:

  • фиксированный список разрешённых include;
  • лимит глубины вложенности;
  • разные лимиты для разных клиентов;
  • запрет тяжёлых связей в списочных endpoint’ах;
  • мониторинг медленных комбинаций;
  • документация стоимости expand-полей.

Также важно различать detail endpoint и list endpoint. Для GET /orders/{id} можно позволить больше вложенных данных. Для GET /orders нужно быть осторожнее, потому что ответ может содержать сотни элементов.

Предотвращение N+1 — это не только задача ORM. Это часть дизайна API. Endpoint должен явно учитывать, какие данные клиенту нужны, сколько записей может быть возвращено и как это повлияет на базу.


Webhook-интеграции

Webhook — это способ уведомить внешнюю систему о событии. Если REST API обычно работает по модели “клиент спросил — сервер ответил”, то webhook работает наоборот: сервер сам отправляет событие клиенту.

Например, после оплаты заказа система может отправить webhook:

Webhook-интеграции должны проектироваться с учётом ненадёжности сети. Нельзя считать, что событие будет доставлено с первой попытки. Клиентский сервер может быть недоступен, отвечать с ошибкой, медленно обрабатывать запрос или вернуть timeout.

Поэтому нужны retries. Обычно используется exponential backoff: повтор через 1 минуту, затем 5 минут, 30 минут, несколько часов. Важно ограничить общее количество попыток и дать клиенту возможность вручную переотправить событие через dashboard или API.

Delivery guarantees почти всегда формулируются как at-least-once delivery. Это означает, что событие будет доставлено минимум один раз, но может быть доставлено повторно. Exactly-once delivery в реальных распределённых системах крайне сложно гарантировать, особенно между разными компаниями и сетями.

Из-за at-least-once delivery клиент обязан обрабатывать webhooks идемпотентно. Для этого каждое событие должно иметь уникальный eventId. Получатель сохраняет обработанные eventId и при повторной доставке не выполняет действие повторно.

Signatures нужны для проверки подлинности webhook. Иначе любой злоумышленник сможет отправить запрос на endpoint клиента и имитировать событие оплаты или изменения заказа. Обычно тело webhook подписывается секретом с помощью HMAC. Клиент пересчитывает подпись и сравнивает её с заголовком, например:

X-Webhook-Signature: ...
X-Webhook-Timestamp: ...

Timestamp нужен для защиты от replay attacks. Если злоумышленник перехватил старый webhook, он не должен иметь возможность отправить его повторно через неделю.

Проблема порядка доставки событий очень важна. Например, клиент может получить order.shipped раньше, чем order.paid, если события доставлялись с разными задержками. Поэтому нельзя слепо полагаться на порядок получения. Нужно передавать createdAt, возможно sequenceNumber, а клиент должен уметь обрабатывать out-of-order events.

Иногда вместо передачи полного состояния в webhook передают только идентификатор ресурса:

А клиент после получения события сам делает запрос к REST API:

GET /orders/ord_1001

Это снижает риск обработки устаревших данных, потому что клиент получает актуальное состояние ресурса. Но увеличивает количество запросов к API.

Хорошая webhook-система должна иметь журнал событий, статусы доставок, повторные отправки, подписи, idempotency, документацию, тестовые события и понятную политику retry.


API для поиска товаров

Поиск товаров — один из самых сложных REST API-сценариев в e-commerce. На первый взгляд endpoint может выглядеть просто:

GET /products?query=phone

Но реальный поиск включает фильтры, сортировку, фасеты, пагинацию, наличие, цены, категории, бренды, скидки, персонализацию и производительность на большом каталоге.

Базовый endpoint может выглядеть так:

GET /products/search?q=iphone&category=phones&brand=apple&priceMin=50000&priceMax=150000&sort=price_asc&page=1&pageSize=20

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

Сортировка тоже должна быть ограниченной. Нельзя позволять клиенту сортировать по любому произвольному полю базы данных. Это опасно для производительности и безопасности. Лучше иметь whitelist:

sort=price_asc
sort=price_desc
sort=popular
sort=newest
sort=rating

Фасеты — это агрегированные значения для фильтров. Например, при поиске ноутбуков API может вернуть доступные бренды, диапазоны цен, объём памяти, размеры экрана и количество товаров по каждому значению:

Фасеты удобны для UI, но могут быть дорогими в вычислении. На большом каталоге их часто считают через поисковые движки вроде Elasticsearch, OpenSearch, Solr или специализированные search-сервисы. Если пытаться строить сложные фасеты только на SQL при большом объёме данных, можно быстро столкнуться с медленными запросами.

Пагинация может быть offset-based или cursor-based.

Offset-based выглядит так:

GET /products?page=5&pageSize=20

Это просто для клиента, но плохо работает на больших offset. Чем дальше страница, тем дороже запрос.

Cursor-based pagination выглядит так:

GET /products?limit=20&cursor=eyJpZCI6...

Она лучше для больших данных и бесконечной прокрутки, но сложнее для UI с переходом на произвольную страницу.

Для поиска товаров часто нужен компромисс. Если каталог небольшой, offset pagination может быть достаточно. Если каталог большой, данные часто обновляются, а пользователь использует infinite scroll, лучше cursor-based.

Проблемы производительности возникают из-за полнотекстового поиска, сложных фильтров, сортировки по неиндексированным полям, фасетов, персонализации, join’ов, проверки остатков и цен. Поэтому search API часто строят не напрямую поверх основной transactional базы, а поверх отдельной search index/read model.

Важно также учитывать консистентность. Если товар обновился в основной базе, поисковый индекс может обновиться с задержкой. Это нормально, если система допускает eventual consistency. Но для цены и наличия могут потребоваться дополнительные проверки перед добавлением в корзину или оформлением заказа.

Хороший REST API для поиска товаров должен быть не просто endpoint’ом к таблице products, а отдельной read-моделью, оптимизированной под пользовательский сценарий поиска.


API для комментариев

Комментарии кажутся простой сущностью, но быстро становятся сложными из-за вложенности, модерации, удаления, сортировки, жалоб, прав доступа и производительности.

Базовая модель комментария может включать:

Для создания комментария можно использовать:

POST /posts/{postId}/comments

Для ответа на комментарий:

POST /comments/{commentId}/replies

Или единый endpoint с parentId:

POST /posts/{postId}/comments

Вложенность — главный архитектурный вопрос. Можно разрешить бесконечную древовидную структуру, но это почти всегда создаёт проблемы: сложные запросы, тяжёлый UI, непредсказуемая глубина, проблемы сортировки и пагинации. Поэтому часто вводят лимит глубины, например 2 или 3 уровня. После этого ответы могут отображаться как плоская ветка или требовать отдельного перехода.

Есть разные способы хранить дерево комментариев:

  • adjacency list: каждый комментарий хранит parentId;
  • materialized path: хранится путь до комментария;
  • nested set: удобно для чтения дерева, сложнее для изменений;
  • closure table: отдельная таблица связей предок-потомок.

Для большинства приложений достаточно adjacency list с ограниченной глубиной и дополнительными индексами. Для сложных форумов или больших деревьев могут потребоваться materialized path или closure table.

Модерация добавляет статусы: pending, published, rejected, hidden, deleted. Важно не смешивать физическое удаление и скрытие. Если комментарий удалён модератором, это не всегда значит, что его нужно удалить из базы.

Soft delete обычно предпочтительнее физического удаления, потому что комментарий может быть частью ветки. Если удалить родительский комментарий физически, что делать с ответами? Часто используют подход:

В UI это отображается как “Комментарий удалён”, но ответы остаются видимыми.

Сортировка комментариев тоже требует проектирования. Возможные варианты:

sort=newest
sort=oldest
sort=popular
sort=top

Для древовидных комментариев сортировка сложнее, потому что нужно сортировать не только корневые комментарии, но и ответы внутри веток. Например, корневые комментарии можно сортировать по популярности, а ответы — по времени.

Пагинация для комментариев должна учитывать структуру дерева. Нельзя просто вернуть первые 100 строк из базы и ожидать корректное дерево. Часто API возвращает корневые комментарии постранично, а для каждого корневого комментария — первые несколько ответов:

А остальные ответы загружаются отдельно:

GET /comments/{commentId}/replies

Это снижает размер ответа и делает UI управляемым.

Также нужно учитывать права доступа. Автор может редактировать свой комментарий только в течение определённого времени. Модератор может скрывать комментарии. Заблокированный пользователь не может комментировать. Некоторые комментарии могут быть видны только администраторам.

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


Заключение

Нестандартные вопросы по REST API обычно появляются тогда, когда система выходит за рамки простого CRUD. Контракты начинают жить годами, клиенты становятся разными, операции становятся массовыми, данные агрегируются, поиск требует отдельной read-модели, webhooks добавляют асинхронность, а безопасность и производительность становятся частью архитектуры.

Хороший REST API — это не просто набор endpoint’ов. Это стабильный контракт, понятная модель ошибок, управляемая эволюция, продуманная совместимость, контролируемая производительность и уважение к клиентам, которые будут использовать этот API в реальных условиях.

Проектируя REST API, важно думать не только о том, как выполнить текущую задачу, но и о том, как этот контракт будет жить через год, как его будут использовать разные клиенты, как он будет меняться, тестироваться, документироваться и защищаться от ошибок.

WitSlice © 2026