26 сент. 2025 г.·7 мин чтения

Границы фронтенда и бэкенда для недопустимых действий

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

Границы фронтенда и бэкенда для недопустимых действий

Почему эта проблема возвращается снова и снова

Продукт часто ломается в одном и том же шве: страница принимает действие, а сервер его отклоняет. Пользователь видит кнопку, форму или настройку, которые кажутся валидными, и продолжает. Через несколько секунд система говорит «нет».

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

Типичный пример — оформление заказа. Страница позволяет применить промокод, показывает меньшую сумму и включает оплату. Сервер позже отказывается принимать код, потому что он истёк, принадлежит другому аккаунту или не совмещается с этим планом. Страница сначала сказала «да», поэтому отклонение кажется багом.

Команды обычно латят видимую часть и продолжают дальше. Добавляют тултип, отключают кнопку или переписывают текст ошибки. Это может помочь на несколько дней, но не решает контракт между экраном и сервером. Если реальное правило живёт в одном бэкенд‑чеке, то то же неверное действие вернётся через другой экран, мобильное приложение, панель администратора или клиента API.

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

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

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

Где обычно ломаются границы

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

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

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

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

То же действие дрейфует между клиентами. Одна страница отправляет { planId: 4 }. Другая — { plan: "4" }. Мобильное приложение может отправлять третью форму. Для человека все эти варианты читаются одинаково, но бэкенду приходится интерпретировать догадки вместо одного чёткого запроса.

Сообщения об ошибке часто скрывают реальную проблему. «Что‑то пошло не так» может означать, что товар просрочен, пользователь лишился прав, форма запроса неправильна или достигнут лимит. Люди повторяют неправильное действие, потому что продукт не говорит им, что именно случилось.

Права доступа создают ещё один разрыв, потому что они могут измениться после загрузки страницы. Админ снимает доступ, пробный период заканчивается или роль в воркспейсе меняется. Если страница не проверяет снова, пользователь всё ещё может открыть модал и нажать «Сохранить». Отклонение приходит поздно и кажется багом, даже если бэкенд прав.

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

Если два экрана запускают одно и то же бизнес‑действие, им нужны одинаковая форма запроса, одинаковые правила валидности и понятные причины отказа.

Сначала смоделируйте ошибки пользователей, прежде чем менять код

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

Начните с действий, а не со страниц. «Нажать отправить дважды» — одно действие. «Загрузить файл до выбора проекта» — другое. «Сохранить с пустым обязательным полем» — третье. Сложите их в один список, даже если они происходят на разных страницах.

Для каждого действия запишите точное состояние, которое видит пользователь прямо перед ним. Кнопка включена? На странице остались старые данные? Пользователь потерял сеть на две секунды? Эти мелочи объясняют, почему действие казалось допустимым.

Здесь разделение становится понятнее. Экран может предупреждать и подсказывать. Сервер должен решать — отклонить, принять или игнорировать действие.

Простое правило помогает:

  • Отклонять, когда действие создаст неверные данные или нарушит порядок.
  • Принимать, когда действие всё ещё валидно, даже если экран выглядит странно.
  • Игнорировать, когда повторение того же действия не должно иметь эффекта, например второй тап на «повторно отправить код» в коротком интервале.

Группируйте ошибки по имени действия, а не по названию страницы. «Удалить элемент без прав» должно быть одним случаем, независимо от того, началось ли это из таблицы, детального вида или комбинации клавиш. Это делает контракт API чище и препятствует тому, чтобы команды латали одну и ту же проблему трижды.

Обычно хватает небольшой общей таблицы:

ActionWhat the user sees before itServer behaviorNotes
Invite teammate with empty emailButton looks active, field has focus errorRejectReturn clear field error
Save draft after item was archivedOld tab still openRejectTell user item is no longer editable
Click "send" twiceSlow spinner, no confirmation yetIgnore duplicateReturn same result as first request

(Таблица выше служит шаблоном: в вашей локальной версии переведите столбцы на русский, если нужно.)

Product, дизайн и инженерия должны работать с одной и той же таблицей. Она даёт продуктовой команде чёткое правило, дизайну — понятное сообщение, а инженерам — контракт, который покрывает неверные действия ещё до того, как начнутся изменения кода.

Исправляйте контракт по одному действию

Начните с одного действия. Не пытайтесь исправить пять экранов одновременно. Выберите что‑то узкое, например изменение email или отмену заказа после оплаты.

Много команд латят кнопку, форму и текст сообщения, но баг остаётся внизу. Один экран блокирует плохой клик, в то время как API всё ещё принимает то же неверное действие с другого экрана, мобильного приложения или старого клиента.

Напишите контракт простым языком:

  1. Определите состояния до начала действия. Для отмены заказа это могут быть «pending», «paid», «packing» и «shipped».
  2. Решите, в каких состояниях действие разрешено, а в каких отклоняется. Храните это правило в API, а не только на экране.
  3. Назовите каждую причину отказа понятным языком, чтобы саппорт мог её прочитать без догадок. «Order already shipped» лучше, чем просто invalid_transition.
  4. Возвращайте одинаковую форму ответа каждый раз. Если API отклоняет запрос, пусть поля, коды и формат сообщения будут одинаковыми во всех клиентах.
  5. Напишите тесты для обоих путей сначала. Один тест докажет, что действие работает в разрешённом состоянии, другой — что API отклоняет его в заблокированном.

Детали важны. Если один endpoint возвращает { code, message }, а другой — { error: { type, text } }, команда экрана начнёт лепить одноразовые исправления. Через месяц никто не будет доверять обработке ошибок.

Простой контракт проще поддерживать. Например, endpoint для смены email всегда может возвращать текущий email, статус и опциональный объект ошибки. Это позволит экрану показывать одни и те же поля после успеха или отказа, вместо того чтобы догадываться, что изменилось.

Эта работа не глянцевая, но она экономит реальное время. Чёткие контракты останавливают мелкие рассогласования, которые превращаются в повторяющиеся тикеты, граничные случаи и молчаливые ошибки данных.

Что должен делать экран после отклонения

Get fractional CTO help
Bring in experienced product and architecture help without hiring a full time CTO.

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

Покажите причину в точке сбоя. Если пользователь нажал «Оплатить сейчас», и заказ провалился из‑за просроченного купона, покажите это сообщение рядом с оплатой или полем купона, а не в расплывчатом баннере вверху. Людям не стоит просматривать всю страницу, чтобы понять, что случилось.

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

Хорошее состояние отклонения обычно делает пять вещей:

  • Оставляет страницу в том же месте.
  • Указывает на точное неудачное действие или поле.
  • Использует причину сервера простым языком.
  • Сохраняет введённые данные, где это возможно.
  • Предлагает один безопасный следующий шаг.

Этот следующий шаг важнее, чем многие думают. Тупик быстро раздражает. Если товар закончился, предложите «удалить товар» или «сохранить на потом». Если сессия истекла, предложите «войти снова» и верните пользователя в тот же шаг. Если проверка прав не прошла, покажите, кто может одобрить действие или где запросить доступ.

Мелочи тоже помогают. Повторно включайте кнопку после отказа, если повтор всё ещё не будет повторять ту же ошибку. Сохраняйте позицию прокрутки. Не закрывайте модал, если внутри нужно что‑то исправить. Если сервер возвращает карту ошибок по полям, используйте её вместо общего toast‑сообщения.

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

Простой пример из checkout

Типичный баг в корзине начинается с промокода. Покупатель вводит код, и страница сразу уменьшает сумму. Браузер угадал, что код валиден, до проверки сервера.

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

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

Страница вела себя так, будто владеет ценой. Сервер по‑прежнему владеет финальной суммой.

Сломанный поток

Допустим, в корзине $80, и покупатель вводит SAVE20. Страница показывает $64, потому что она только проверяет формат кода или наличие в кэше. Покупатель нажимает «Оплатить» и ожидает завершения за несколько секунд.

Затем сервер проверяет код по текущим правилам и обнаруживает, что акция закончилась вчера. Сервер имеет другую итоговую сумму, чем страница. Если поток неаккуратный, оплата падает с сообщением вроде "transaction failed" или "unable to process order". Это бесполезно.

Лучший контракт

Исправление — не красивая ошибка, а более хороший API‑контракт для валидации промокодов. Когда покупатель применяет код, сервер должен вернуть:

  • чёткий результат, например expired_discount_code
  • правильную текущую итоговую сумму
  • обновлённую сумму скидки, если она есть
  • достаточно данных корзины, чтобы страница могла перерисовать сводку

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

Это небольшое изменение делает поток спокойнее. Покупатель видит, что изменилось, почему и что делать дальше.

Частые ошибочные патчи

Clean up rejection paths
Keep user input, show clear errors, and stop late failures from feeling random.

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

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

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

Общие ошибки создают вторую кучу проблем. Команды переиспользуют одно сообщение для разных сбоев, потому что это просто. Пользователи видят «Что‑то пошло не так», саппорт получает расплывчатые тикеты, и продуктовые команды не понимают, что именно сломалось. Просроченный купон, проблема с правами и отсутствие товара требуют разных ответов.

Дрейф возникает, когда мобильная и веб‑версии следуют разным правилам. Одно приложение блокирует дубликаты, другое отправляет два запроса. Одно обрезает пробелы в поле, другое сохраняет их. Пользователям всё равно, какой клиент вызвал баг — они видят только случайное поведение.

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

Быстрая проверка ловит большую часть проблем:

  • Поместите правило на сервер в первую очередь.
  • Возвращайте специфичную ошибку для каждого отказа, который пользователь может исправить.
  • Тестируйте одно и то же действие с web, mobile, админ‑инструментов и фоновых процессов.
  • Решите, что старые клиенты должны получать при столкновении с новым правилом.

Если правило важно, все клиенты должны сталкиваться с одним и тем же ограничением. Экран может направлять людей, но сервер должен проводить линию.

Быстрые проверки перед релизом

Review older client paths
Check stale tabs, old clients, admin tools, and direct API calls before release.

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

Короткая предрелизная проверка ловит самые дорогие ошибки:

  • Попробуйте недопустимое действие через API, а не только через UI. Если прямой запрос всё ещё работает, правило находится не там.
  • Сравните web, mobile, админ‑инструменты и фоновые задания. Они должны отправлять одни и те же поля для одного и того же действия, иначе сервер начнёт угадывать, что значит запрос.
  • Читайте каждый ответ об отказе так, будто вы пользователь. Одной чёткой причины всегда лучше, чем расплывчатое «Request failed».
  • Принудительно вызовите отказ и посмотрите, что делает форма дальше. Сохраните корректный ввод, отметьте поле или шаг, который провалился, и не заставляйте вводить всё заново.
  • Проверьте логи для неуспешного запроса. В логах должно быть состояние, которое вызвало отказ: просроченная корзина, заблокированная запись или пропущенный шаг подтверждения.

Мелкие рассогласования здесь важны. Если один клиент отправляет "quantity", а другой — "count", сервер может отклонять один путь и принимать другой. Пользователям это кажется случайностью, но обычно это проблема контракта, а не интерфейса.

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

Логирование заслуживает такого же внимания. «Validation error» слишком мало для кого‑то. Записывайте, какое правило не прошло, какое состояние объекта вызвало это и какой клиент отправил запрос. Это даёт разработчикам конкретику до того, как тикеты саппорта накопятся.

Что делать дальше

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

Прежде чем кто‑то поменяет UI, опишите контракт простым языком. Пропустите технические термины в первом черновике. Фраза вроде «Если промокод истёк, сервер отклоняет заказ, оставляет корзину неизменной и возвращает причину, которую экран может показать как есть» гораздо полезнее, чем расплывчатая заметка о валидации.

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

Короткий обзор обычно быстро находит проблему:

  • выберите один поток с максимальной болью для саппорта
  • перечислите все недопустимые действия простым английским (или вашим локальным языком)
  • перенесите бизнес‑правило в API, если сейчас его решает экран
  • определите точную форму ответа и что делает экран дальше

Это также хорошее время пересмотреть старые endpoint'ы. Если UI всё ещё прячет кнопки, отключает формы или переписывает текст ошибок, чтобы навязать правило, контракт, скорее всего, слишком слаб. Экран должен направлять пользователя, но сервер должен решать.

Если нужен внешний обзор, Oleg Sotnikov на oleg.is работает как внештатный CTO и стартап‑советник — и такого рода «очистка» контрактов как раз то, с чем он помогает командам.

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

Часто задаваемые вопросы

Почему недостаточно только исправить кнопку?

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

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

Кто должен решать недопустимые действия — фронтенд или бэкенд?

Пусть бэкенд решает, действительно ли действие допустимо. Он владеет данными и видит реальное состояние в момент запроса.

Фронтенд должен предупреждать заранее, показывать текущее состояние и объяснять ошибки простым языком.

Что здесь означает общий контракт?

Контракт — это правило, которому обе стороны следуют для одного действия. Он описывает форму запроса, когда действие разрешено, когда отклоняется и как выглядит ответ.

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

Как справляться со «старыми» состояниями страницы?

Рассматривайте устаревшее состояние как норму, а не как редкость. Пользователь может оставить вкладку открытой, потерять доступ или увидеть изменение цены до клика.

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

Что должен возвращать API при отклонении действия?

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

Например, при ошибке с купоном отправьте обновлённую итоговую сумму или текущий статус товара вместо расплывчатого «request failed».

Что должен делать экран после ответа «нет» от сервера?

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

Если платёж не прошёл из‑за истёкшего купона, оставьте корзину и адрес, покажите проблему с купоном рядом с оплатой, а не в общей шапке.

Как предотвратить двойные клики и повторную отправку?

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

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

Насколько конкретными должны быть сообщения об ошибках?

Пишите сообщения, которые говорят людям, что произошло и что делать дальше. «Купон истёк» или «У вас больше нет доступа» лучше, чем «Что‑то пошло не так».

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

Что тестировать перед релизом?

Сначала протестируйте недопустимое действие на границе API. Если прямой запрос всё ещё проходит, правило находится не там, где надо.

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

С чего начать, если таких проблем несколько?

Выберите один поток, который уже создаёт тикеты, теряет продажи или требует ручной работы. Часто это checkout, регистрация, возвраты или приглашения.

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