Перейти к основному содержимому

Управление сетевыми запросами (HTTP / API Layer)

Цель

Инфраструктурный слой (HTTP / API) отвечает только за выполнение HTTP-запросов и обработку аспектов (baseURL, headers, retries, interceptors, отмена). Он не управляет состоянием данных и не знает о UI.

Управление жизненным циклом данных с сервера описано в разделе "Управление Server State" регламента


Общие принципы

MUST

  • Вся логика сетевого взаимодействия находится только в слое API: src/shared/api/*.
  • В проекте есть единый HTTP-клиент (singleton / factory), который:
    • выполняет HTTP-запросы
      • централизованно конфигурируется (baseURL, timeout, headers)
      • поддерживает middleware/interceptors
      • перехватывает неуспешные ответы (4xx/5xx)
      • логирует сетевые ошибки
      • поддерживает отмену запросов (AbortController)
  • Все запросы строго типизированы.

SHOULD

  • API слой предоставляет тонкие сервисы по доменам (например usersApi, ordersApi) без бизнес-логики.
  • Ошибки приводятся к единому формату (ApiError) для удобной обработки выше по стеку.

FORBIDDEN

  • Использовать HTTP-клиент напрямую вне src/shared/api/*.
  • Реализовывать бизнес-логику внутри src/shared/api/*.
  • Знать про UI-детали в API слое (toast, router, store, components).

Допустимые библиотеки HTTP-клиента

  • Xior
  • Wretch
  • Ky

В проекте должна быть выбрана одна библиотека и один единый клиент.


Контракт API и типизация

MUST

  • Каждый endpoint имеет типы Request/Response (DTO).
  • Допустимые источники типов:
    • сгенерированные DTO из OpenAPI
    • вручную описанные DTO: /packages/api или src/shared/api/dto

FORBIDDEN

  • Использовать any, unknown, object, {} в DTO-типах.
  • Использовать inline-типы в сигнатурах запросов.

Стиль экспорта API методов

Функциональный стиль (именованные экспорты) разрешён и часто удобнее в больших проектах, если соблюдать правила слоя shared/api.

// ✅ Хорошо (functions + строгая типизация + DTO)
import type { UserDto, UpdateUserDto } from '@/shared/api/dto';

export function getUserById(id: string, signal?: AbortSignal) {
return http.get<UserDto>(`/users/${id}`, { signal });
}

export function updateUser(id: string, dto: UpdateUserDto, signal?: AbortSignal) {
return http.put<UserDto>(`/users/${id}`, { json: dto, signal });
}

Почему это хорошо:

  • Все типы вынесены в DTO (UserDto, UpdateUserDto)
  • Нет any/unknown/object/{} в DTO
  • Есть поддержка отмены (AbortSignal)
  • Нет бизнес-логики, только транспортный вызов
// ✅ Хорошо (functions + единый контекст запроса)
import type { UserDto } from '@/shared/api/dto';

export function searchUsers(params: { q: string; limit?: number }, signal?: AbortSignal) {
return http.get<UserDto[]>('/users/search', {
searchParams: params,
signal
});
}
// ❌ Плохо (inline types + any)
export const getUserById = (id: string) => http.get<any>(`/users/${id}`);

export const updateUser = (id: string, dto: { name: string }) =>
http.put(`/users/${id}`, { json: dto });

Почему плохо:

  • any ломает API и отключает типобезопасность выше по стеку
  • Inline DTO ({ name: string }) разъезжается между файлами и усложняет рефакторинг
  • Нет AbortSignal, поэтому труднее правильно управлять отменой запросов
// ❌ Плохо (бизнес-логика внутри shared/api)
import type { UserDto } from '@/shared/api/dto';

export async function getUserAndRedirectIfMissing(id: string) {
const user = await http.get<UserDto>(`/users/${id}`);

if (!user) {
router.push('/404'); // ❌ UI/Router зависимость в shared/api
}

return user;
}

Почему плохо:

  • shared/api не должен зависеть от UI/Router/Store
  • Логика "что делать в UI" должна находиться выше (server state / feature / page)

Почему функции часто лучше

  1. Лучше для tree-shaking и бандла
  2. Именованные экспорты проще оптимизируются сборщиком, чем большой объект, который импортируют целиком.
  3. Чище API и проще импорты
import { getUserById, updateUser } from '@/shared/api/users/users.api';
// VS
import { usersApi } from '@/shared/api/users';

С функциями легче "тащить только нужное", особенно когда файлов много. Проще тестирование и моки:

  • функции проще мокать точечно (jest.mock/vi.mock по именам)
  • меньше риска "случайно замокали весь объект"

Рекомендация для большого проекта

MUST

  • Функции в shared/api/* строго типизированы и используют DTO.
  • Нет any/unknown/object/{} в DTO.
  • Нет UI/Router/Store зависимостей в shared/api.

SHOULD

  • Один файл = один домен (users.api.ts, orders.api.ts, billing.api.ts)
  • Экспорт только именованных функций (или дополнительно index.ts как public API домена)
  • Опционально: принимать AbortSignal во всех чтениях/модификациях

MAY

  • Использовать объект-стиль, если команде так удобнее, при сохранении всех ограничений.

Архитектурные ограничения (границы)

MUST

  • shared/api не зависит от:
    • UI (components/pages/widgets)
    • доменных модулей (features/entities)
    • state management (redux/zustand)
    • роутера (next/router, react-router)
  • shared/api зависит только от:
    • types/dto
    • shared/lib (helpers)
    • env/config (baseURL, headers) (если есть)

Структура

✅ Хорошо (поток зависимостей)
UI/Features -> Server State (Tanstack/Query) -> shared/api -> HTTP client -> network

❌ Плохо
shared/api -> store/router/components

Request cancellation (AbortController)

MUST

  • API методы принимают AbortSignal (optional).
  • Server State слой отвечает за отмену/жизненный цикл.

Пример

✅ Хорошо
// shared/api/contacts/contacts.api.ts
getContactById(id: string, signal?: AbortSignal) {
return http.get<UserDto>(`/users/${id}`, { signal });
}

Временные отклонения (MVP / small projects)

Допускается (временно)

  • упрощенная конфигурация HTTP-клиента
  • отсутствие централизованного логирования
  • минимальная типизация ответов (но без any)

Обязательные ограничения (всё равно MUST)

  • сетевые вызовы локализованы в одном месте (shared/api)
  • UI не выполняет HTTP-запросы напрямую
  • бизнес-логика не размещается рядом с HTTP-клиентом

Когда возвращаемся к основным правилам

  • появление нескольких доменных модулей
  • повторное использование API-кода
  • рост количества запросов/интеграций

Процедуры контроля (Code Review)

Ревьюер должен убедиться, что:

  • HTTP-клиент используется только внутри слоя shared/api;
  • Отсутствуют импорты HTTP-клиентов (fetch, axios и аналогов) вне shared/api;
  • Бизнес логика не реализуется внутри shared/api;
  • Слой shared/api не зависит от UI и доменных модулей.

Автоматизация (lint / boundary checks)

MUST

В проекте настроены проверки, запрещающие:

  • прямое использование fetch/axios/вне src/shared/api/
  • импорт HTTP-клиента вне src/shared/api/*
  • нарушения границ модулей (например shared/api импортирует features/*)

Пример правил:

  • eslint rule / custom rule: запрет fetch вне shared/api
  • boundary checks (eslint-plugin-boundaries / custom):
    • контроль направлений импортов

TODO

  • пример структуры shared/api (httpClient, interceptors, errors)
  • пример ApiError унификации и mapping ошибок
  • пример ESLint правил с кодом для запрета fetch/axios вне shared/api