Всем привет, меня зовут Сергей Прощаев. Я Tech Lead в FinTech и преподаватель в OTUS. Сегодня — не лекция, а диагностика. За последние два года на собеседованиях 90% аналитиков уверенно рассуждают про REST, но путают PUT и PATCH, возвращают 200 при создании ресурса или забывают про версионирование. Поэтому три коротких кейса из практики — решите сами, а потом сверимся.

Важно понимать: в API‑дизайне редко существует единственно правильный ответ. Обычно это компромисс между HTTP‑семантикой, удобством клиентов, зрелостью платформы и стоимостью поддержки. Мои разборы — это аргументированная позиция, а не истина в последней инстанции.

Рис. 1. Диагностика: сможете ли вы спроектировать API, которое не сломается через полгода?
Рис. 1. Диагностика: сможете ли вы спроектировать API, которое не сломается через полгода?

Кейс № 1. Метод и идемпотентность: простая операция, которая ломает production

Условие.
Вы проектируете API для сервиса управления подписками. Нужно реализовать операцию «отключить автопродление» для активной подписки пользователя. Бэкенд‑разработчик говорит: «Сделаем POST /subscription/{id}/cancel-auto-renewal — это действие, оно не идемпотентно, каждый запрос отключает опцию». Продакт добавляет: «А если клиент по ошибке нажмёт кнопку дважды, ничего страшного — пусть просто вернёт успех».

Остановитесь и выберите вариант (можно мысленно):

  1. Оставить POST /subscription/{id}/cancel-auto-renewal — клиент сам отвечает за дубли.

  2. Использовать PUT /subscription/{id}/auto-renewal с телом {enabled: false} — при повторном вызове эффект тот же.

  3. Использовать DELETE /subscription/{id}/auto-renewal — удаляем свойство «автопродление».

  4. Сделать PATCH /subscription/{id} и передавать {"autoRenew": false}.

Пауза. Я специально поставил этот кейс первым, потому что на реальных собеседованиях люди чаще всего выбирают вариант 1 или 4, но не могут объяснить, почему один лучше другого.

Разбор.

  • Вариант 1 (POST с глаголом в URL) — это action‑oriented API‑подход, ближе к RPC‑стилю. Он широко используется в индустрии (Stripe, Kubernetes, AWS) и сам по себе не является ошибкой. Но у него есть недостатки: сложнее обеспечивать единообразие семантики, идемпотентность и предсказуемость поведения API по мере роста системы.

  • Вариант 2 (PUT на подресурс auto-renewal) — технически идемпотентен, PUT заменяет состояние ресурса целиком. Но здесь есть нюанс: подресурс auto-renewal — это логическое свойство основного ресурса subscription. Создавать отдельный «ресурс» для одного булева поля — избыточно, хотя и допустимо. Я такое видел в API некоторых CRM‑систем, и это работает, но увеличивает количество эндпоинтов.

  • Вариант 3 (DELETE /auto-renewal) — тоже идемпотентен. DELETE здесь выглядит спорно не потому, что подресурс невозможен (с точки зрения REST ресурсом может быть любая адресуемая концепция), а потому что семантика операции становится неоднозначной. Клиенту неочевидно: мы удаляем настройку автопродления, отключаем флаг или удаляем связанную сущность. Для простого переключения состояния PATCH читается понятнее.

Правильный вариант (4) — PATCH /subscription/{id} с телом {"autoRenew": false}.

Почему?

  • PATCH предназначен для частичного изменения ресурса. Важно понимать: RFC не гарантирует идемпотентность PATCH. Однако конкретная PATCH‑операция может быть идемпотентной, если повторный запрос приводит к одному и тому же состоянию ресурса. В нашем случае {"autoRenew": false} — именно такой сценарий.

  • Для простого изменения одного поля PATCH выглядит проще и компактнее. Отдельный подресурс имеет смысл, когда у настройки появляется собственный lifecycle, права доступа или отдельные операции (например, /users/1/mfa).

  • Клиенту легко включить автопродление обратно: PATCH с {"autoRenew": true}.

Техническое уточнение: RFC PATCH не навязывает конкретный формат тела. Но на практике чаще используют стандартизированные варианты: JSON Patch (RFC 6902) или JSON Merge Patch (RFC 7386). В нашем примере подойдёт JSON Merge Patch, если зафиксировать это в контракте.

Какой навык проверяли: умение выбрать HTTP‑метод с учётом идемпотентности и семантики, а не «как удобнее написать контроллер».

Из практики. Однажды мы сделали «быстрый» API для биллинга на POST /cancel-subscription. Всё работало полгода, пока не появился автоматический retry на клиенте. В день релиза нам прилетело: дважды вызванный POST отправлял второе уведомление в поддержку, создавал дубль в логах и запускал цепочку лишних событий. Со временем API начал дрейфовать в сторону RPC‑style интерфейса: появились десятки action‑endpoint«ов с разной семантикой и непредсказуемым поведением при повторных запросах и идемпотентности. Проблему решили за неделю, переведя критичные операции на идемпотентные методы. С тех пор я всегда спрашиваю на собеседованиях: „А что будет, если клиент отправит запрос дважды?“.»

Кейс № 2. Коды ответа и ошибки: 200 или 400?

Условие.
У вас есть API для регистрации пользователя: POST /users. Клиент отправляет email и пароль. Бизнес‑правило: email должен быть уникальным. Если пользователь с таким email уже существует, API должен вернуть ответ.

Остановитесь и выберите вариант (можно мысленно):

  1. 200 OK и в теле {"status": "error", "message": "Email exists"}.

  2. 400 Bad Request.

  3. 409 Conflict.

  4. 422 Unprocessable Entity.

Мои наблюдения. Примерно 40% аналитиков на интервью называют 400 или 409, но не могут объяснить разницу между 400 и 409. А 20% вообще честно говорят: «На прошлом проекте мы всегда возвращали 200 с полем success: false».

Разбор.

  • Вариант 1 (200 с ошибкой) — классическое «API как RPC». Для REST API это обычно считается плохой практикой, потому что ломает семантику HTTP и усложняет обработку ошибок на стороне клиента. Однако в некоторых интеграционных или legacy‑сценариях (например, GraphQL, асинхронная оркестрация) такой подход всё ещё встречается.

  • Вариант 2 (400 Bad Request) означает, что запрос синтаксически неверен или не соответствует схеме (например, поле email не строка). Но здесь запрос корректен с точки зрения структуры — проблема в бизнес‑логике (уникальность). 400 слишком общий, разработчики клиентов начнут путать ошибки валидации полей и бизнес‑конфликты.

  • Вариант 3 (409 Conflict) — рекомендованный. Статус говорит: «Запрос конфликтует с текущим состоянием ресурса». Конфликт уникальности email — классика. RFC 7231 прямо указывает, что 409 подходит для случаев, когда запрос не может быть выполнен из‑за конфликта с состоянием сервера. Добавляем в тело понятное сообщение {"error": "email_already_exists", "message": "User with this email already registered"}— и клиент знает, как обработать.

  • Вариант 4 (422 Unprocessable Entity) активно используется многими современными API и фреймворками (Rails, FastAPI, GitHub API). Проблема в другом: в индустрии нет единого консенсуса, где проходит граница между 400409 и 422. Поэтому важно не столько выбрать «идеальный» статус, сколько обеспечить единообразие внутри API‑платформы.

Как я обычно делаю:

  • 400 — для ошибок валидации на уровне полей (email не соответствует regex, пароль короткий).

  • 409 — для бизнес‑конфликтов, которые клиент мог бы предотвратить (email уже занят, заказ уже оплачен, документ заблокирован).

  • 200 с ошибкой — только в legacy или специфических интеграционных сценариях, но не в новом REST API.

Какой навык проверяли: понимание семантики HTTP‑статусов и различие между синтаксической ошибкой, бизнес‑конфликтом и успешным запросом.

Из практики. Помню историю: в одном финтех‑проекте мы использовали 409 для блокировки счетов, когда два запроса пытались списать деньги одновременно. Без этого статуса клиент не понимал, что нужно повторить запрос с обновлёнными данными. А с 409 — спокойно отрабатывал через retry.

Кейс № 3. Версионирование: URL, заголовок или что‑то ещё?

Условие.
Через полгода после запуска API вам нужно добавить новую функциональность в эндпоинт GET /users/{id}/orders. Старый формат ответа: {orderId, date, amount}. В новой версии требуется добавить поле status(Изменения типа amount нет — только additive change, что потенциально обратно совместимо для клиентов, реализованных по tolerant reader pattern.)

Остановитесь и выберите вариант (можно мысленно):

  1. Добавить новую версию в URL: GET /v2/users/{id}/orders.

  2. Использовать custom заголовок: API-Version: 2.

  3. Использовать Accept‑заголовок с медиатипом: Accept: application/vnd.mycompany.v2+json.

  4. Не версионировать, а просто добавить поле status в старый ответ (обратная совместимость).

Мой личный опыт. Я пробовал все варианты.

Разбор.

  • Вариант 1 (URL) — самый простой для понимания, его любят разработчики и DevOps. Главный минус URL‑версионирования — версия становится частью идентификатора ресурса, из‑за чего один и тот же бизнес‑ресурс начинает существовать по разным URI. С практической точки зрения это обычно приемлемо, но теоретически расходится с «чистым» REST‑подходом.

  • Вариант 2 (custom заголовок) — гибкий, но нестандартный. Прокси и кеширующие серверы могут игнорировать его, и вы рискуете, что старые клиенты случайно получат новую версию.

  • Вариант 3 (Accept‑медиатип) — ближе к классическому content negotiation подходу из REST. Он позволяет сохранить URI стабильным и даёт возможность версионировать отдельные представления ресурса. Недостаток: сложнее в тестировании и мониторинге (плохо видно в логах nginx).

  • Вариант 4 (additive change) — технически обратно совместим, если клиенты используют tolerant reader pattern и игнорируют неизвестные поля. Многие API поступают именно так (например, добавление status в ответ заказа). Но если клиенты применяют строгую валидацию схемы, они могут упасть. Поэтому «просто добавить поле» — это риск, зависящий от экосистемы.

Мой выбор. Для большинства внутренних B2B‑проектов я предпочитаю URL‑версионирование (/v1/.../v2/...). Оно прозрачное, быстрое и снижает риск ошибок. А если API публичное и требует долгосрочной поддержки множества версий — использую Accept с медиатипом.

Что не рекомендуется: класть версию в query параметр (?version=2). Это часто создаёт проблемы с кешированием, маршрутизацией и наблюдаемостью API, поэтому query‑versioning обычно считается нежелательным.

Уточнение про индустрию: в индустрии встречаются разные подходы. GitHub исторически использовал vendor media types через Accept‑заголовок, а Stripe — версионирование через отдельный заголовок Stripe-Version. Универсального стандарта де‑факто не существует: выбор зависит от требований к обратной совместимости, инфраструктуре и сроку жизни API.

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

Живой пример. На одном e‑commerce проекте мы поленились версионировать и добавили новое поле grossAmount в ответ заказа. Через неделю клиенты с жёсткой схемной валидацией начали падать после появления нового поля. В итоге пришлось разворачивать параллельный эндпоинт под старый формат и переводить клиентов по заголовку. С тех пор версионирование закладываю в API с первого дня — даже если сейчас кажется, что «ничего не изменится».

Две схемы, которые структурируют мышление

Чтобы закрепить материал, посмотрим на две простые, но полезные визуализации. Они помогут вам быстрее принимать решения при проектировании API.

Вот первая — про версионирование. В кейсе № 3 вы сравнивали варианты, а теперь увидите, как технически работает маршрутизация запросов в зависимости от выбора: URL‑версионирование или Accept‑медиатип. Схема наглядно показывает, где именно «встраивается» версия и почему это влияет на мониторинг и чистоту API (рис.2):

Рис. 2. Сравнение подходов к версионированию: URL vs Accept-заголовок
Рис. 2. Сравнение подходов к версионированию: URL vs Accept‑заголовок

Что выносим из этой схемы: URL‑версионирование (/v1/...) просто для инфраструктуры, но версия становится частью идентификатора ресурса. Accept‑медиатип чище с точки зрения REST, но сложнее в мониторинге. Выбор зависит от того, публичное это API или внутреннее, и как долго вы планируете поддерживать старые версии.

А вторая схема — это алгоритм выбора HTTP‑статуса при ошибке. Во втором кейсе мы спорили, стоит ли возвращать 200 с ошибкой, 400, 409 или 422. Эта схема — мой рабочий чек‑лист. Она проведёт вас по шагам: сначала проверяем синтаксис, потом бизнес‑логику, потом права доступа. Держите её под рукой, когда будете проектировать новый эндпоинт (рис. 3):

Рис. 3 — алгоритм выбора HTTP-статуса при ошибке
Рис. 3 — алгоритм выбора HTTP‑статуса при ошибке

Что выносим из этой схемы: сначала проверяем синтаксис и схему (статус 400). Затем бизнес‑конфликт (409). Потом аутентификацию и авторизацию (401/403). И не используем 200 с ошибкой в теле для новых REST API.

Финальный чек‑лист перед тем, как отдать API в разработку

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

  • URL идентифицирует ресурс, а не действие (никаких /createUser/getData).

  • HTTP‑метод соответствует операции (GET для чтения, POST для создания, PUT/PATCH для обновления, DELETE для удаления).

  • Идемпотентность продумана: повторный PUTPATCHDELETE не вызывает побочных эффектов.

  • Коды ответов используются по назначению (201 при создании, 202 для асинхронных, 204при успешном удалении без тела).

  • Ошибки имеют тело с понятным кодом (error_code) и сообщением, а статус отражает суть (400409422404).

  • Версионирование заложено с первого дня — хотя бы на уровне маршрутов (/v1/...).

  • Есть спецификация OpenAPI, и она не врёт.

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


А теперь — что со всем этим делать? Где брать системный подход?

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

Такие кейсы хорошо показывают отдельные пробелы, но не дают полной картины. Для более объективной оценки можно пройти бесплатный тест и посмотреть, какие компетенции уже сформированы, а над какими ещё стоит поработать.

Задача системного аналитика — не просто нарисовать красивую диаграмму с Use Case. Это спроектировать API так, чтобы оно не ломалось через полгода, не вызывало споров о статусах, имело внятное версионирование и, главное, было удобно использовать. На курсе «Системный аналитик. Advanced» мы разбираем такие кейсы десятками. Не только REST, но и event‑driven архитектуры, интеграции с legacy, управление требованиями к API и, конечно, полный цикл документирования через OpenAPI.

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

  • 11 июня в 20:00. «Создаём ИИ-ассистента для системного аналитика за 1 час». Записаться

  • 22 июня в 20:00. «OAuth 2.0, JWT и коварные куки: Проектируем безопасную аутентификацию». Записаться

  • 25 июня в 20:00. «Какие навыки прокачать, чтобы стать экспертом в системном анализе в 2026 году». Записаться

Больше бесплатных уроков июня смотрите в дайджесте.

Комментарии (9)


  1. SDWa
    08.06.2026 13:50

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


    1. Cordekk
      08.06.2026 13:50

      Так не надо поддерживать все 20 версий, даже более того, это может быть невозможно.

      Обычно даётся время на переход на новую версию, а потом старую закрывают.

      Иногда старые версии воообще не поддерживаются, сразу после изменений их убирают.


  1. totsamiynixon
    08.06.2026 13:50

    Повторю и;в этой статье. Откройте АПИ Слак или Страйп - там все POST или GET. Все методы (включая PATCH/PUT/DELETE можно реализовать через комбинацию POST + заголовки ответа со стороны сервера и тело запроса). Идемпотентность достигается не потому что вы использовали PATCH или PUT и операция внезапно стала идемпотентной, а потому что либо операция естественным образом спроектирована быть идемпотентной (условный SET), либо если есть ключ идемпотентности (условно проверка через WHERE).

    Не зря HTML5 поддерживает только POST & GET.

    PUT/PATCH/DELETE нужны именно в браузере для реализации кеширования - DELETE даёт браузеру понять, что надо очистить локальный кэш, PUT сделать реплейс кеша и ТД. Если вы делаете Machine to Machine это вообще не нужно.


    1. verls
      08.06.2026 13:50

      ИМХО здесь присутствует разница между подходом использовать HTTP с его заголовками и его контент как API и второй подход оставлять HTTP просто транспортом и реализовывать API в самом контенте. Мне нравится первый вариант, кажется более универсальным/простым. Второй тоже можно выбрать в специфических случаях, когда простой схемой API не обойтись и нужно оперировать многоуровневыми JSONами.


      1. totsamiynixon
        08.06.2026 13:50

        Каждой задаче свой инструмент. Я только за то, чтобы использовать нативный HTTP. Конкретно в этой статье просто вызывает возмущение попытка называть что-либо "правильным" или "неправильным" с аргументами вроде "солнце жёлтое потому что когда я смотрю на небо я вижу его жёлтым и во всех книжках и мультиках оно описывается как жёлтое" и упуская вообще фактор земной атмосферы и что оно на самом деле белое.


  1. yunas
    08.06.2026 13:50

    Мне кажется, пример с идемпотетностью не совсем корректный. Бэкэндер может реализовать и PUT-метод так, что он будет генерировать side-effect'ы в виде лишних событий. Задача аналитика как раз продумать и предупредить такие кейсы. Напротив, можно реализовать RPC-style вполне идемпотетно


  1. tema_rebel
    08.06.2026 13:50

    Идемпо-* (вот это слово) зависит от рук программиста, а не от глагола ендпоинта.

    Уже в первом примере сломал себе голову... Гет на чтение, пост на обновление. Всё!

    Хотите повыпендриваться - назовите мне lifecycle записей в таблицах GC и условия перехода между ними.


  1. tema_rebel
    08.06.2026 13:50

    А ещё подскажите, что за позиция такая - «аналитик»? Иногда ещё пишут «системный аналитик». Раньше (лет 10 назад) урлами занимались архитектор и техлид, при этом был бизнес аналитик, который карточки заполнял в джире. «Системный аналитик» из статьи очень похож как раз таки на технаря - знать, куда какой ендпоинт пойдёт и что произойдёт дальше... При этом это вроде и не технарь. Вообще не понимаю эту роль -(

    Как техлид/архитектор, я не позволю никому (кроме архитекторов и сто) говорить мне, какие урлы делать)) тем более, если у аналитика 2-3 года опыта (как обычно пишут в вакансиях).


    1. NataliaSa
      08.06.2026 13:50

      вот-вот. А я не понимаю почему меня как аналитика заставляют разрабам писать урлы вместо донесения бизнес-нюансов:). Как-то все перевернулось с ног на голову