Слой API в приложении: сервисы и типизация
↑ 5.4 Работа с API: REST, GraphQL, WebSocket · ← Предыдущая · → Следующая
Зачем это на собесе
Как организовать обращения к 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.
Практика
- Вынесите вызовы API из компонентов в
api/*.tsи хуки Query. - Добавьте маппер DTO → модель (даты, деньги).
- Настройте MSW и протестируйте слой без реального сервера.
Вопросы с ответами
Зачем отдельный слой API?
Единая точка изменений, общая обработка ошибок/авторизации, типизация и лёгкое тестирование.
Зачем мапперы DTO?
Изолируют UI от формы ответа сервера и преобразуют данные (даты, деньги) в удобные модели.