Экран может показать «готово», получить ответ 200 и всё равно сломаться на следующем клике. Причина обычно не в HTTP: сервер вернул успешный статус, но поле переименовали, массив действий стал строкой или messageCode исчез из одного ответвления. Цена ошибки — рассинхрон между UI и API: пользователь видит устаревшее состояние, а команда ищет проблему в сети, хотя нарушение уже произошло на границе данных.
Граница должна отвечать на два разных вопроса. Первый: завершился ли обмен по HTTP? Второй: можно ли безопасно использовать представление для конкретного экрана? Нельзя сводить их к одному boolean ok. В этой статье разберём минимальную screen model, проверим её на чистой функции и разложим действия при статусах 2xx. Подход одинаково применим в браузерном клиенте, BFF и интеграционном тесте.
HTTP-успех и успех контракта
RFC 9110 описывает значение статус-кода как часть HTTP-сообщения. Код 200 сообщает, что запрос обработан успешно на уровне протокола и приложения, но конкретное содержимое ответа всё равно определяется контрактом ресурса. Клиент не получает права угадать форму по имени endpoint или по тому, что предыдущая версия возвращала похожий JSON.
Поэтому полезно держать в коде последовательность: сначала принять статус, затем проверить media type, потом разобрать JSON, после этого валидировать поля и только в конце передать результат в view-model. Порядок кажется длиннее прямого await response.json(), но он локализует ошибку. Если payload не соответствует договорённости, компонент не обязан угадывать fallback и не превращает частично прочитанные данные в доменный факт.
| Поле | Тип | Зачем UI | Что делать при ошибке |
|---|---|---|---|
| status | ready | pending | blocked | выбрать состояние экрана | не выбирать состояние по умолчанию молча |
| allowedActions | массив известных команд | показать доступные действия | скрыть команды и записать нарушение |
| messageCode | непустая строка | выбрать локализованное сообщение | показать безопасный общий текст |
| Content-Type | application/json | понять формат представления | не вызывать JSON parser вслепую |
| HTTP status | целое 100–599 | разделить transport и application result | зафиксировать код до чтения тела |
Контракт должен быть маленьким, но решаемым
Полная DTO предметной области редко нужна экрану. Если компонент читает двадцать полей, это не означает, что screen model должна копировать все двадцать. Выберите минимальный набор, который позволяет принять решение: состояние, разрешённые команды и ключ сообщения. Любое поле должно иметь владельца и правило изменения. Это снижает связность: backend может менять внутреннее представление, не заставляя UI разбирать чужой aggregate.
При этом «минимальный» не значит «неформальный». Для каждого значения задайте допустимый словарь или тип. status: "ok" удобен только до появления второго смысла слова ok. Перечень ready | pending | blocked делает расширение видимым: новый статус потребует решения о рендеринге, тесте и обратной совместимости. Массив действий также должен быть закрытым или версионируемым; неизвестная команда не должна появляться как активная кнопка.
Проверка до рендера
import { checkScreenResponse } from './upgrade-2026-09.mjs';
const response = {
status: 'ready',
allowedActions: ['edit'],
messageCode: 'order.ready',
};
console.log(checkScreenResponse(response));
// { valid: true, errors: [], source: 'response-boundary-validator' }
console.log(checkScreenResponse({ status: 'done', allowedActions: 'edit' }));
// { valid: false, errors: ['status-must-be-known', ...], source: 'response-boundary-validator' }Функция принимает копию JSON-совместимого значения и возвращает причины, а не исключение с произвольным текстом. Это не замена JSON Schema или типам на этапе сборки: runtime-проверка нужна потому, что HTTP приносит данные из-за границы процесса. В реальном клиенте результат следует связать с error boundary, telemetry без payload и понятным сообщением для пользователя. Самое важное — не передавать невалидное значение в компонент, который считает его достоверным.
Не стоит делать validator чрезмерно умным. Он не должен сверять бизнес-правила, запрашивать второй endpoint или самостоятельно исправлять поле. Если status пришёл как "ready ", trim может скрыть нарушение контракта. Исправление допустимо на границе, только если это явно часть формата: например, нормализация регистра для заголовка. Чем больше молчаливых преобразований, тем труднее понять, что на самом деле отправил сервер.
204, JSON и пустое тело
Классическая ошибка — общий helper, который всегда вызывает response.json(). Для 204 это некорректное ожидание: успешный ответ не обязан иметь representation. Команда «удалить» может завершиться 204 и потребовать повторного чтения списка; команда «получить экран» обычно возвращает представление. Эти два случая нельзя различать по URL или по тому, что parser иногда падает. Правило должно быть частью операции.
Заголовок Content-Type тоже не подтверждает, что JSON валиден. Он сообщает заявленный формат, а не соответствие вашему screen contract. Поэтому проверка media type — ранняя ветка, а runtime validation — следующая. Ошибку формата следует отделять от сетевой ошибки: повтор запроса не исправит payload, который сервер стабильно формирует неправильно.
Рантайм-проверка и OpenAPI
OpenAPI удобна как единый источник описания HTTP-интерфейса: она помогает связать ответ операции с компонентной схемой и генерировать типы. Но сгенерированный TypeScript-интерфейс не проверяет JSON во время выполнения. Если сервисы релизятся независимо, нужен контрактный тест или runtime validator на границе. Иначе компилятор подтвердит только то, что разработчик написал в исходниках.
JSON Schema может описать типы, обязательность и перечисления, а валидатор — применить эту схему к фактическому payload. В небольшом клиенте допустима ручная функция, как в примере, если у неё есть тесты на неизвестный статус, неправильный массив и пустой message code. Выбор библиотеки — вторичен. Сначала зафиксируйте, какую ошибку должен увидеть пользователь и какие данные нельзя пропускать в view.
Как внедрить границу без большого переписывания
- Найдите один endpoint, где UI уже угадывает поля или использует fallback после исключения parser.
- Выпишите screen model из фактических решений компонента: состояние, доступные действия и сообщение.
- Добавьте проверки статуса и Content-Type до разбора тела; отдельно обработайте 204.
- Добавьте runtime validation для обязательных полей и неизвестных enum-значений.
- Напишите четыре теста: валидный ответ, неизвестный статус, неверный тип actions и успешный ответ без JSON.
- Покажите пользователю безопасное состояние ошибки, а в технический канал передайте только код нарушения и request id.
Ограничения и следующий шаг
Валидатор не доказывает, что значение истинно в домене, и не заменяет authorization. Он проверяет форму ответа и право клиента использовать эту форму. Он также не решает миграцию старого поля: для этого нужны версия схемы, совместимый период и тесты обеих сторон. Если один endpoint обслуживает несколько экранов, лучше назвать две read model, чем снова передать в UI универсальный объект.
Следующий практический шаг — добавить в контракт тест с неизвестным статусом и проверить, что компонент не показывает «готово» по умолчанию. После этого полезно сравнить generated types с runtime schema и зафиксировать расхождение в CI. Важный результат — не ещё один helper, а видимая граница, после которой UI работает только с проверенной моделью.
Проверяемые источники
- RFC 9110: HTTP Semantics — RFC 9110, июнь 2022. Использован для различения семантики статус-кода и содержимого representation. Граница: Не описывает screen model конкретного продукта и не заменяет runtime validation.
- OpenAPI Specification 3.1.1 — версия 3.1.1, 24 октября 2024. Использован как формат описания HTTP-операции и схемы ответа. Граница: Сгенерированный тип не является проверкой фактического JSON.
- RFC 9457: Problem Details for HTTP APIs — RFC 9457, июль 2023. Использован для идеи структурировать ошибки HTTP отдельным типом. Граница: Не назначает локальные коды UI и не выбирает retry policy.