ADR-UI-012. API-клиент и модели
UI ходит к бэку по REST. Контракт описан OpenAPI; клиент к нему генерируется, а не пишется руками. Но UI не должен зависеть от API-моделей напрямую — иначе любая правка контракта расходится по компонентам и шаблонам. Фиксируем: где живёт и как регистрируется сгенерированный клиент, как устроены UI-модели, где и как выполняется маппинг API↔UI, и что весь трафик идёт через клиент.
1. Контекст и постановка задачи
Фронт обращается к бэку по REST. Контракт описан OpenAPI-спекой, и клиент к нему генерируется
(swagger-codegen, src-ui/swagger-codegen/generate.py), а не пишется вручную. Прямая зависимость
UI от сгенерированных API-моделей опасна: переименование поля, смена формата даты, вложенные DTO —
и правка расползается по компонентам и шаблонам.
Нужно зафиксировать:
- как генерируется, где лежит и как регистрируется клиент;
- как организованы UI-модели и чем они отличаются от API-моделей;
- где и как выполняется маппинг API ↔ UI;
- что всё сетевое взаимодействие идёт через клиент (в т. ч. загрузка файлов).
Контракт — наш (бэк в этом же репозитории), поэтому эндпоинты описываются так, чтобы генератор выдавал рабочие типизированные методы, а не подгоняются под ограничения инструмента задним числом.
2. Драйверы решения
- Изоляция UI от мутаций контракта. Правка API — точечное изменение маппинга, не сквозная замена по шаблонам.
- Минимум boilerplate. Слой изоляции не должен плодить файлы ради самого себя — маппинг простой и локальный.
- Прозрачность. Понятно, где тип, где маппинг, где запрос.
- Единый контракт ответа. Все ручки возвращают одну обёртку (ADR-SRV-001) — фронт разворачивает её единообразно.
- Совместимость с per-page паттерном (ADR-UI-013): запрос+маппинг+эффекты — в сервисе страницы.
3. Рассмотренные варианты
- A. UI работает прямо на API-моделях. Минимум кода, максимум связности с контрактом — любая его правка бьёт по компонентам.
- B. Свои UI-модели; маппинг — всегда в отдельных mapper-файлах. Чисто, но заводит слой-файл на каждый DTO даже там, где он не нужен.
- C. Свои UI-модели; маппинг — статической функцией в per-page service, отдельный
<feature>.mapper.tsвводится только при росте/переиспользовании. Локально, без лишних файлов.
4. Решение
Выбран Вариант C.
4.1. Сгенерированный клиент
- Генерируется из OpenAPI бэка (
src-ui/swagger-codegen/generate.py), один контекст —icd. - Место:
src/app/services/api-clients/icd/; импорт по алиасу@api/icd(ADR-UI-003). - Не редактируется руками. Любые правки — в бэке/контракте и перегенерации.
- Регистрация в DI — явная. Сгенерированный
ApiModuleне используем (приложение standalone) — нужные api-сервисы перечисляются вprovidersapp.config.ts. Базовый URL — токенBASE_PATHизRuntimeConfigService(ADR-UI-014).
Чеклист после перегенерации: просмотреть api-clients/icd/api/api.ts (список сервисов) и
добавить в providers app.config.ts каждый api-сервис, используемый в per-page сервисах.
Пропуск даёт NG0201: No provider for _XxxService при первом обращении.
4.2. UI-модели
- Общие (не привязанные к фиче) — в
src/app/models/:Pagination,Sorting,Option<T>и т. п. - Доменные — в
modules/<feature>/models/<entity>.ts(имя типа — PascalCase сущности, без инфраструктурного префикса; имена файлов — по ADR-UI-002).
UI-модель — это то, что удобно показывать и редактировать. Типичные отличия от API-модели:
денормализация (плоские lookup-карты вместо вложенных ссылок), удобные форматы (enum-код → подпись,
ISO-строка → отображаемая дата), поля «только для UI». Покрывать все поля API-модели она не обязана —
только используемые страницей. Пример — справочники ввода (modules/input/models/input-reference.ts),
наполняемые из @api/icd маппером.
4.3. Маппинг API ↔ UI
- База — статическая функция
toUiв per-page service:api.method(...).pipe(map(res => toUi(res.payload))). - Вынос в
modules/<feature>/<feature>.mapper.ts— когда функция разрослась (≈>30–40 строк) или одна сущность маппится в нескольких сервисах фичи (переиспользование). Пример уже вынесенного —modules/input/models/input-reference.mapper.ts. - Направления:
toUi(ApiX → X, чтение) иtoApi(X → команда, запись; часто отдельные функции под create/update — состав полей команд различается). - Шаблоны видят только UI-модели. Импорт
@api/*в шаблонах/компонентах-представлениях запрещён.
4.4. Обёртка ответа
Все ручки возвращают единый контракт IcdBaseRequestResult (ADR-SRV-001): payload / isSuccess
/ errors[]. Per-page service разворачивает payload, проверяет isSuccess, а при ошибке
показывает errors[].errorMessage — тексты формирует бэк по-русски, UI их не переводит
(ADR-UI-015).
4.5. Весь трафик — через слой api-clients
Per-page service инжектирует сгенерированный api-сервис и зовёт его методы; он не строит
URL/HttpClient/FormData сам. Это касается и загрузки файлов: так как контракт наш,
эндпоинт описывается корректным multipart/form-data (файлы — бинарные поля), и генератор выдаёт
типизированный метод (напр. apiInputPacketPostForm(payload, files)), внутри которого сам собирает
FormData. Корректность multipart-описания на стороне бэка обеспечивается отдельно (см. связанный
backend-артефакт).
4.6. Загрузка данных
- Методы per-page service возвращают
Observable<UI-модель>/Observable<UI-модель[]>; API-модели за пределы сервиса не выходят. - Компонент подписывается через
takeUntilDestroyed()и кладёт результат в сигнал (ADR-UI-008) либо используетtoSignal().
4.7. Чего не делаем
- Не используем API-модели в шаблонах.
- Не правим сгенерированный клиент руками — только перегенерация.
- Не тащим прямой
HttpClient/FormData/apiBaseUrlв per-page service (текущий обход на загрузке пакета — миграционный долг, S8). - Не делаем универсальный авто-маппер — явные функции дешевле в поддержке.
- Не кэшируем данные между страницами (при появлении проблемы — отдельный ADR).
5. Положительные следствия
- Правка API-контракта — точечное изменение функции маппинга, не сквозная по компонентам.
- UI-модели читаются как «то, что на экране», без шума серверных DTO.
- Per-page service — единая точка запроса, маппинга и эффектов одной страницы.
- Один контракт ответа и один способ разворачивать
payload/errorsна всех страницах.
6. Отрицательные следствия и компромиссы
- Дублирование структуры: API-модель + UI-модель + функция маппинга. Для тривиальных сущностей избыточно — принимаем как цену изоляции.
- При множестве маппингов в одной фиче сервис разрастается — мониторим и выносим в
<feature>.mapper.ts. - Требование «контракт под генератор» иногда обязывает поправить описание эндпоинта на бэке (напр. корректный multipart), а не обходить его на фронте.
7. Проверка
- В шаблонах нет импортов из
@api/*; шаблоны ссылаются только на UI-модели. - Per-page services возвращают типы из
modules/<feature>/models/илиapp/models/, не из@api/*. - Каждый инжектируемый api-сервис перечислен в
providersapp.config.ts— открытие страницы не даётNG0201. - Переименование поля в контракте → перегенерация клиента → правка одной функции маппинга → код собирается.
- Загрузка файлов идёт через сгенерированный метод клиента, а не через прямой
HttpClient+FormData.
8. Открытые вопросы / отложено
- Миграционный долг — сдача пакета. Сейчас
modules/input/services/data-input-page.service.tsшлётPOST /api/InputPacketнапрямуюHttpClient+FormData(историческое следствие некорректного multipart-описания на бэке). Бэк исправлен — перегенерировать клиент, зарегистрироватьInputPacketServiceвapp.config.tsи перевести сервис наapiInputPacketPostForm(payload, files). - Auth-ошибки (401/403). Глобальная обработка interceptor-ом появится с auth-ADR; до тех пор ошибки ловит сам per-page service.
- Помощник
serverErrors → поля формы(ADR-UI-009) — введём при первой форме с серверной валидацией. - Кэширование частых справочников — отложено (пока каждая страница грузит заново).