Перейти к содержанию

HttpClient и локальный fixture API

До этого список опирался на импортированный массив. Теперь введём HTTP-границу, сохранив возможность изучать Angular самостоятельно. TicketsApi будет отправлять GET и PATCH через HttpClient, а в режиме fixture функциональный interceptor обработает их внутри браузера. В режиме remote те же запросы дойдут до Nest. Результат главы — разделение компонента, транспорта и источника данных без необходимости запускать серверный курс раньше клиентского.

Fixture здесь означает учебную реализацию известного договора. Она хранит три заявки в памяти, использует искусственную задержку и возвращает такие же формы Ticket или TicketList, как будущий сервер. Это не запись HTTP на диске и не копия PostgreSQL. Между вкладками нет общего состояния, при перезагрузке коллекция восстанавливается, а журнал ticket_events не создаётся. Смысл fixture — изучать клиентские состояния и обработку ответов, честно сохраняя ограничения источника.

Транспортный сервис

Ниже полный класс TicketsApi из src/app/api.ts; импорты HttpClient, HttpParams, Injectable, inject, config и функции readList/readTicket находятся в том же файле архива. Это фрагмент составного файла, потому что рядом объявлены authInterceptor и errorMessage. Не переносите класс отдельно без этих зависимостей. List формирует параметры через HttpParams, get и patch кодируют id для адреса и получают unknown, который затем проверяется функцией модели.

@Injectable({providedIn:'root'})
export class TicketsApi {
  private readonly http = inject(HttpClient);
  list(q: string, status: string) {
    let params = new HttpParams();
    if (q) params = params.set('q',q);
    if (status) params = params.set('status',status);
    return this.http.get<unknown>(`${config.apiBase}/tickets`,{params}).pipe(map(readList));
  }
  get(id: string) { return this.http.get<unknown>(`${config.apiBase}/tickets/${encodeURIComponent(id)}`).pipe(map(readTicket)); }
  patch(id: string, value: TicketPatch) { return this.http.patch<unknown>(`${config.apiBase}/tickets/${encodeURIComponent(id)}`,value).pipe(map(readTicket)); }
}

HttpParams не изменяет существующий объект методом set: возвращается следующее значение. Поэтому переменная params присваивается снова. Если игнорировать результат, запрос уйдёт без ожидаемого q или status. Пустой фильтр вообще не включается в query; он означает отсутствие ограничения. Для API это яснее, чем status с неизвестным значением «all», которого нет в договоре DTO. Привычные подписи интерфейса не должны незаметно стать дополнительными машинными статусами.

Ожидаемый GET /api/tickets?q=проектор вернёт items с rq-101 и total равным одному. До подписки Observable ещё не выполняет эту операцию. Компонент выбирает, когда начать чтение и как связать его с жизненным циклом страницы. TicketsApi не сохраняет результат внутри себя и не управляет спиннером: он описывает транспортную операцию и проверку её формы. Это позволяет одному вызову быть использованным в списке, а другому — в карточке без общего случайного флага loading.

Проверка unknown

Тип get<Ticket>() означал бы обещание разработчика TypeScript, а не выполнение runtime-проверки JSON. Мы выбираем unknown и map(readTicket), чтобы проверить поля на границе. В readList также проверяются массив, total и уникальность id. Повреждённый ответ не попадает в template как заведомо корректная заявка. Он становится ошибкой операции, которую страница объяснит отдельно. При этом проверка допускает дополнительные поля ответа: строгий запрет лишних входных DTO-полей на сервере имеет другую цель.

ReadTicket проверяет id, title, status, положительную safe integer version и разбираемую дату updatedAt. Это минимальная защита учебной wire-модели. Она не доказывает, что заявка принадлежит текущему пользователю, что дата пришла из доверенного сервера или что title безопасен в любом контексте. Авторизация и предметная доступность решаются сервером. Обычная интерполяция заголовка в Angular остаётся выбранным способом вывода; ручной innerHTML из title здесь не вводится.

Ошибку модели нельзя путать с пустой коллекцией. Ответ {items:[],total:0} корректен и обозначает отсутствие совпадений. Ответ {items:"нет",total:0} нарушает контракт и должен остановить построение состояния ready. Если без проверки подставить второй ответ в цикл, проблема проявится далеко от транспорта, например в шаблоне. Раннее объяснение причины помогает найти несовпадение API и клиента, а не исправлять разметку под произвольный повреждённый JSON.

Порядок interceptors

ProvideHttpClient получает withInterceptors с authInterceptor первым и fixtureInterceptor вторым. Первый добавляет текущий публичный demo token только к адресам /api. Второй в fixture-режиме перехватывает подготовленный запрос и проверяет учебную роль. Если поменять их порядок, fixture может не увидеть добавленное Authorization, поскольку сам завершается ответом, не вызывая next. Порядок здесь имеет функциональный смысл и должен сохраняться в app.config.ts.

Interceptors не меняют исходный request напрямую: authInterceptor использует clone с setHeaders. Внешние запросы не получают наш Authorization, поскольку правило ограничено apiBase. Это особенно полезно при развитии приложения с другим источником ресурсов: учебный токен не должен случайно отправиться на любой URL только из-за использования общего HttpClient. В текущем архиве apiBase равен /api; изменение базы требует повторно проверить условие адресов и proxy, а не только одну строку маршрута.

Fixture откладывает обработку через timer, а мутация выполняется после срабатывания таймера внутри defer. Благодаря этому отменённая до обработки подписка не изменяет локальную базу. Это модель клиентского упражнения, а не гарантия поведения реального сервера после отмены HTTP: удалённый PATCH мог уже выполниться. В следующих главах мы будем отдельно рассматривать отмену чтения и неоднозначный исход записи. Один удобный fixture не должен стереть это различие из договора.

При самостоятельном воспроизведении смените config.mode на remote и сохраните те же адреса. Angular dev-server передаст /api через proxy на 127.0.0.1:3000. Если Nest ещё не подготовлен, ожидается ошибка соединения, а не автоматическое возвращение к fixtures. Такой отказ полезен: он явно сообщает, какой источник выбран. Лаборатория не скрывает недоступный сервер локальной копией данных и не утверждает, что искусственная задержка отражает реальные характеристики сети.

Вы уже можете проследить операцию: страница вызывает метод сервиса, сервис описывает HTTP, interceptor выбирает учебный или удалённый путь, map проверяет ответ, страница принимает результат. Дальше добавим поток адресного фильтра и отмену устаревшего чтения. Сама модель Ticket и точки /api/tickets останутся прежними, поэтому новая тема будет относиться к времени результатов, а не к очередной замене исходного проекта.

Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.

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