Архитектура
$fetch.create для HTTP запросов
Контекст
Нужен единый способ работы с API с поддержкой авторизации и обработки ошибок.
Решение
Использовать $fetch.create() для создания преконфигурированного HTTP-клиента.
Документация Nuxt:
- https://nuxt.com/docs/4.x/guide/recipes/custom-usefetch
- https://nuxt.com/docs/4.x/examples/advanced/use-custom-fetch-composable
Обоснование
| Критерий | $fetch.create |
|---|---|
| Встроен в Nuxt | Да (ofetch) |
| Типизация | Отличная |
| Interceptors | onRequest, onResponse, onResponseError |
| SSR/SPA | Работает везде |
| Bundle size | 0 KB (встроен) |
Паттерн: Plugin + Composable
1. Plugin для создания $api instance
// app/plugins/api.ts
export default defineNuxtPlugin((nuxtApp) => {
const { token, refreshToken, logout } = useSession()
const config = useRuntimeConfig()
const api = $fetch.create({
baseURL: config.public.apiBase,
onRequest({ options }) {
if (token.value) {
options.headers.set('Authorization', `Bearer ${token.value}`)
}
},
async onResponseError({ response }) {
if (response.status === 401) {
const refreshed = await refreshToken()
if (!refreshed) {
await nuxtApp.runWithContext(() => navigateTo('/login'))
}
}
},
})
return {
provide: {
api,
},
}
})
2. Composable useAPI
// app/composables/useAPI.ts
import type { UseFetchOptions } from 'nuxt/app'
export function useAPI<T>(
url: string | (() => string),
options?: UseFetchOptions<T>,
) {
return useFetch(url, {
...options,
$fetch: useNuxtApp().$api as typeof $fetch,
})
}
3. Использование
// В компонентах — реактивный useFetch
const { data: users, pending } = await useAPI<User[]>('/users')
// Для мутаций — напрямую $api
const { $api } = useNuxtApp()
await $api('/users', { method: 'POST', body: userData })
4. useFetch vs useAsyncData
| Функция | Когда | Особенности |
|---|---|---|
useAPI() (обёртка useFetch) | GET-запросы к API | Автоматический ключ кэша по URL, реактивный URL |
useAsyncData() | Сложная логика получения данных | Ручной ключ кэша, любая async-функция |
$api (напрямую) | Мутации (POST/PUT/DELETE) | Без кэша, без SSR deduplication |
// useFetch — когда URL является ключом кэша
const { data: users } = await useAPI<User[]>('/users')
// useAsyncData — когда нужна сложная логика или зависимые запросы
const { data: dashboard } = await useAsyncData('dashboard', async () => {
const [stats, recent] = await Promise.all([
$api('/stats'),
$api('/activity?limit=10'),
])
return { stats, recent }
})
// Зависимые запросы — watch для автообновления
const userId = ref<string>()
const { data: profile } = await useAPI<User>(
() => `/users/${userId.value}`,
{ watch: [userId] }, // рефетч при смене userId
)
// Ленивая загрузка — не блокирует рендер
const { data: comments, pending } = await useAPI<Comment[]>('/comments', {
lazy: true,
})
// Ручной рефетч — для refresh кнопок
const { data, refresh } = await useAPI<User[]>('/users')
// <UButton @click="refresh()">Обновить</UButton>
5. Пагинация
// app/composables/usePaginatedAPI.ts
export function usePaginatedAPI<T>(url: string, pageSize = 20) {
const page = ref(1)
const { data, pending, refresh } = useAPI<{ items: T[]; total: number }>(
() => `${url}?page=${page.value}&limit=${pageSize}`,
{ watch: [page] },
)
const totalPages = computed(() =>
Math.ceil((data.value?.total ?? 0) / pageSize),
)
return { data, pending, page, totalPages, refresh }
}
Альтернатива: useAuthFetch в auth-layer
Для auth-layer используем упрощённый паттерн:
// auth-layer/composables/useAuthFetch.ts
export function useAuthFetch() {
const { token, refreshToken, logout } = useSession()
const config = useRuntimeConfig()
return $fetch.create({
baseURL: config.public.apiBase,
onRequest({ options }) {
if (token.value) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token.value}`,
}
}
},
async onResponseError({ response }) {
if (response.status === 401) {
const refreshed = await refreshToken()
if (!refreshed) {
logout()
}
}
},
})
}
Публичные vs Авторизованные запросы
// Публичные — $fetch напрямую
const services = await $fetch('/api/public/services')
// Авторизованные — useAuthFetch() или $api
const authFetch = useAuthFetch()
const users = await authFetch<User[]>('/users')
Composable на сущность
// app/composables/useOrders.ts
export function useOrders() {
const authFetch = useAuthFetch()
return {
getAll: (params?: OrderParams) =>
authFetch<PaginatedResponse<Order>>('/orders', { query: params }),
getById: (id: string) =>
authFetch<Order>(`/orders/${id}`),
create: (data: CreateOrderDto) =>
authFetch<Order>('/orders', { method: 'POST', body: data }),
update: (id: string, data: UpdateOrderDto) =>
authFetch<Order>(`/orders/${id}`, { method: 'PATCH', body: data }),
delete: (id: string) =>
authFetch(`/orders/${id}`, { method: 'DELETE' }),
}
}
Конфигурация
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:3001/api',
},
},
})
Обработка ошибок
import { FetchError } from 'ofetch'
try {
await authFetch('/orders', { method: 'POST', body: data })
} catch (error) {
if (error instanceof FetchError) {
if (error.statusCode === 422) {
// Validation errors
const errors = error.data?.errors
}
if (error.statusCode === 409) {
// Conflict
}
}
}
Последствия
Положительные
- Нативное решение Nuxt (ofetch)
- $fetch.create для преконфигурации
- Автоматический token refresh
- Типизация из коробки
- runWithContext для навигации в async callbacks
Отрицательные
- Нет (встроенное решение)
Ссылки
Связанные решения
- adr-003-shared-modules — auth-layer
- standard-api-client