Слой API в приложении: сервисы и типизация

↑ 5.4 Работа с API: REST, GraphQL, WebSocket · ← Предыдущая · → Следующая

Обязательно~3 мин чтенияУровень: middle

Зачем это на собесе

Как организовать обращения к API, чтобы код был типобезопасным и легко тестируемым.

Объяснение

Компоненты не вызывают fetch/axios напрямую: между ними и сетью стоит слой API.

src/api/
  client.ts        # axios/ky-инстанс, интерсепторы, ApiError
  orders.ts        # ordersApi: list/get/create/update/remove
  schemas/         # Zod-схемы ответов (по необходимости)
  types.ts         # DTO / сгенерированные типы
  queries/         # useOrdersQuery, useCreateOrder (Vue Query)
// api/orders.ts
export const ordersApi = {
  list: (params: OrdersFilter, signal?: AbortSignal) => http.get<Page<OrderDto>>("/orders", { params, signal }).then(r => r.data),
  get: (id: string, signal?: AbortSignal) => http.get<OrderDto>(`/orders/${id}`, { signal }).then(r => r.data),
  create: (dto: CreateOrderDto, key = crypto.randomUUID()) => http.post<OrderDto>("/orders", dto, { headers: { "Idempotency-Key": key } }).then(r => r.data),
};
 
// api/queries/orders.ts
export const orderKeys = { all: ["orders"] as const, list: (f: OrdersFilter) => [...orderKeys.all, "list", f] as const, detail: (id: string) => [...orderKeys.all, id] as const };
export const useOrders = (filter: MaybeRefOrGetter<OrdersFilter>) =>
  useQuery({ queryKey: computed(() => orderKeys.list(toValue(filter))), queryFn: ({ signal }) => ordersApi.list(toValue(filter), signal), select: mapOrders });
export const useCreateOrder = () => { const qc = useQueryClient(); return useMutation({ mutationFn: ordersApi.create, onSuccess: () => qc.invalidateQueries({ queryKey: orderKeys.all }) }); };

Принципы:

ПринципЗачем
Один слой доступазамена библиотеки/URL в одном месте, единые интерсепторы
DTO ↔ доменные модели (мапперы)UI не зависит от формы ответа сервера (переименования, даты в Date, деньги)
Типы из контракта (OpenAPI/GraphQL codegen)синхронизация с бэкендом
Ключи запросов — фабрикипредсказуемая инвалидация
Ошибки нормализованыUI работает с ApiError
Отмена (signal)без гонок
Тестируемостьподмена слоя (MSW / моки модулей)
Runtime-валидация на границезащита от неожиданных ответов (Zod)

Не смешивайте: слой API (транспорт) → Vue Query хуки (кэш) → компоненты (UI). Логика форматирования и бизнес-правила — в мапперах/доменных функциях.

Нюансы и подводные камни

  • «Умный» слой с бизнес-логикой становится вторым бэкендом.
  • Дублирование типов вручную расходится с сервером — генерируйте.
  • Мапперы должны быть чистыми и покрытыми тестами.
  • Жёсткая связь компонентов и структуры ответа мешает эволюции API.

Практика

  1. Вынесите вызовы API из компонентов в api/*.ts и хуки Query.
  2. Добавьте маппер DTO → модель (даты, деньги).
  3. Настройте MSW и протестируйте слой без реального сервера.

Вопросы с ответами

Связанные темы