11 KiB
HTTP API и ошибки
Источник контракта
Backend генерирует OpenAPI из собственного публичного API. Этот сгенерированный OpenAPI — исполняемый источник HTTP-контракта. Frontend использует предоставленный OpenAPI snapshot как актуальный вход и локально генерирует типизированный клиент. Backend владеет контрактом, а подготовка и доставка схемы находятся вне frontend. Это разделение принято в ADR 002 и ADR 014.
Backend командой npm run openapi:generate создаёт openapi.json без запуска
сервера и не хранит результат в Git. Contract test сравнивает вывод генератора
с Nest runtime schema настроенного приложения. Frontend хранит полученный
artifact у себя и проверяет соответствие generated types этой копии. CI не
скачивает соседние репозитории и не сравнивает их текущие revisions. Обновление
входного artifact потребителя выполняется отдельно с review; совместимость
развёрнутых версий не доказывается одним зелёным локальным CI.
API имеет prefix /api/v1; Swagger публикуется для локальной разработки по
environment policy, а не используется frontend в runtime.
Скринер акций
POST /api/v1/stocks/screener/search всегда создаёт новый результат по
условиям скринера, включая пустой подтверждённый список. Ответ первой страницы
содержит непрозрачный searchId, время получения каталога и признак следующей
страницы. GET /api/v1/stocks/screener/search/{searchId}?page=... читает
последующую страницу того же результата, не запускает новый поиск и сохраняет
состав и время списка. Идентификатор результата принадлежит текущему пользователю
и его подключению; недоступный или чужой результат возвращает ошибку, а не
пустой список. Точные поля и статусы определяет backend-generated OpenAPI.
Инвестиционные данные и синхронизация
Затронутые GET инвестиционных ресурсов возвращают сгенерированный contract
{ data, meta }. data nullable: null означает отсутствие complete result,
а подтверждённый пустой массив является available complete result. meta
независимо описывает freshness, completeness, lastSuccessfulAt,
resource-relevant lastAttempt и availability. lastAttempt имеет состояния
none, running, succeeded, partial и failed; его неуспех не заменяет
сохранённый complete result или его время. Portfolio.isStale остаётся
специфичным для snapshot полем, а не заменой состояния попытки.
Только Accounts дополнительно возвращает top-level sync: null, если у
текущего подключения ещё не было часовой попытки, иначе connection-wide
наблюдаемый результат. Это наблюдение, не команда. Обычный GET и retry после
проверки доступа читают локальное состояние и не начинают личную работу.
Устаревший sync=observe, пока он сохранён для совместимости маршрута, также
только читает. Request-started screen-sync command/status не является текущей
AS-IS моделью.
Безопасные ошибки завершённой часовой попытки передаются в lastAttempt и
Accounts sync; HTTP transport/access/input errors сохраняют Problem Details.
Старые данные при временной source failure сохраняют исходное успешное время.
Ни одно поле не раскрывает connection revision. Детали поведения —
свежесть и полнота.
Envelope несовместим с прежними bare-array ответами: backend/frontend поставляются согласованной парой. Финальный artifact передаётся явно; локальная генерация и CI frontend читают только собственный snapshot.
Income analytics
GET /api/v1/brokerage-accounts/{accountId}/income-analytics и общий маршрут
all возвращают результат только выбранного периода: итоги по исходным
валютам, точные записи выплат, разбиение по инструментам и временные
интервалы. Данных сравнения с предыдущим периодом или годом ранее в ответе
нет — ни в теле, ни среди зависимостей meta.dependencies, роль которых
ограничена значением selected. Публичную машинную схему задаёт
backend-generated OpenAPI; frontend получает её обновлённый snapshot отдельно.
Точную семантику поведения описывает доход.
Validation и identity
Backend преобразует и проверяет входные данные на общей HTTP-границе. Неизвестные свойства запроса удаляются; сам факт их передачи не обещает отказа. Проверка формата не заменяет бизнес-ограничения и проверку доступа к счёту. Аутентификация подтверждает личность пользователя; accountId из URL сам по себе не доказывает доступ к брокерским данным.
Error contract
Общая граница обработки HTTP-ошибок возвращает application/problem+json
со status и публичным code.
Неожиданный exception возвращает 500 / INTERNAL_SERVER_ERROR, без исходного
message/stack. Типичный ответ:
{ "status": 401, "code": "UNAUTHORIZED" }
Для input validation контракт требует 400, VALIDATION_ERROR и errors как
массив { pointer, detail }, где pointer адресует поле запроса. Это требование
проверяется реальным HTTP-ответом после всех стадий обработки запроса. Глобальная
валидация передаёт фильтру структурированные ошибки без исходных значений и DTO,
а фильтр формирует JSON Pointer, включая вложенные поля. HTTP suites подтверждают
детали ошибок регистрации, банковской карты и синхронизации; OpenAPI отдельно
фиксирует форму ответа.
Ответ не включает автоматически все поля RFC (type, title, instance).
Добавление обязательного поля является изменением публичного контракта.
Клиент выбирает поведение по публичному коду, а не по внутреннему тексту ошибки.
Domain errors и auth
Специальный code определяется сценарием; общее преобразование ошибок и безопасная форма ответа принадлежат HTTP-границе. Auth endpoints используют свои status и cookie operations: Authentication. T-Bank token failure и временная недоступность имеют разные значения: Connection API. Raw upstream/Prisma errors и secrets не передаются frontend.
Наблюдаемость
Backend пишет структурированные JSON-события в stdout через Pino. Обработка
HTTP-запроса получает серверный request ID, который возвращается заголовком
X-Request-ID; допустимый входящий идентификатор сохраняется отдельно как
client request ID. Это позволяет сопоставить публичную ошибку с обработкой
запроса без передачи внутреннего exception в ответ API.
Журнал завершения запроса содержит шаблон маршрута, метод, статус, длительность и при наличии публичный error code. События внешней границы добавляют операцию, результат и число попыток. Разрешённые поля ограничены; полные запросы и ответы, token, cookie и Authorization не являются данными журнала. Фильтрация полей и redaction не защищают секрет, вручную включённый в свободный текст сообщения или stack.
Выбор logger и граница ответственности описаны в ADR 008; согласованное поведение — в workspace-спецификации Structured logging. Сборщик, срок хранения журналов и alerting не определяются приложением.