useApiFetch
Usage
Use the auto-imported useApiFetch composable for API requests. Built on top of Nuxt useFetch, it provides automatic authentication, data unwrapping, business status code checking and unified error handling.
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
// Basic usage (automatically unwraps the data field)
const { data, pending, error, refresh } = await useApiFetch<User[]>('/users')
// POST request
const { data } = await useApiFetch('/users', {
method: 'POST',
body: { name: 'test', email: 'test@example.com' }
})
// Use a different endpoint
const { data } = await useApiFetch('/users', { endpoint: 'v2' })
</script>
useApiFetchinherits all features of NuxtuseFetchwhile providing additional API integration.- Automatically retrieves the token from the session and adds it to request headers (via the
$apiinstance). - Automatically checks business status codes and throws errors.
- Built-in Toast notifications with customizable configuration.
Data Request Patterns
Movk Nuxt provides three data request composables with identical signatures — choose based on execution timing:
| Composable | Execution | Blocks Navigation | Use Case |
|---|---|---|---|
useApiFetch | SSR + CSR | Yes | Above-the-fold core data |
useLazyApiFetch | SSR + CSR | No | Secondary / non-above-the-fold data |
useClientApiFetch | CSR only | No | Non-SEO-sensitive data |
useApiFetch fires the request during SSR, delivering data on first render:
{
"id": "me",
"name": "Movk Demo",
"email": "demo@movk.dev",
"avatar": "/avatar.png",
"role": "developer",
"issuedAt": "2026-06-29T20:53:18.517Z"
}<script setup lang="ts">
interface Profile {
id: string
name: string
email: string
role: string
}
const { data, pending, error, refresh } = await useApiFetch<Profile>('/profile')
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2 flex-wrap">
<UBadge v-if="pending" color="warning" variant="subtle">
pending
</UBadge>
<UBadge v-else-if="error" color="error" variant="subtle">
error
</UBadge>
<UBadge v-else color="success" variant="subtle">
ready
</UBadge>
<UButton size="sm" variant="outline" icon="i-lucide-refresh-cw" @click="refresh()">
刷新
</UButton>
</div>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ error ? { error: error.message } : data }}</pre>
</div>
</template>
Data Unwrapping and Transformation
useApiFetch already receives the business data unwrapped by $api; the generic T directly declares that data type. transform receives the unwrapped data for a secondary transformation:
// Generic T directly declares the business data type (the type of the data field)
const { data } = await useApiFetch<User>('/user')
// data.value = { id: 1, name: 'test' }
// Combine with transform for a secondary transformation
const { data } = await useApiFetch<{ content: User[] }, SelectItem[]>('/users', {
transform: ({ content }) => content.map(u => ({ label: u.name, value: u.id }))
})
$api page for interactive examples of the unwrapping mechanism and skipUnwrap / skipBusinessCheckBusiness Status Code Checking
The $api interceptor automatically checks the business status code and throws an ApiError (see below) when the code is not in successCodes. Pass skipBusinessCheck: true to skip validation (no ApiError is thrown), but the dataKey field is still unwrapped:
const { data } = await useApiFetch('/external', {
skipBusinessCheck: true
})
Error Classification
The module only produces one custom error type — ApiError (business errors) — all others are ofetch's native FetchError. Distinguish scenarios with isBusinessError and statusCode. Select an error type in the dropdown to observe the shape of error.value:
business: HTTP 200 butcodenot insuccessCodes→ApiError,isBusinessErroris true.http-400/http-500: NativeFetchErrorwithstatusCodeset to the corresponding code.http-422:FetchErrorwhosedatafield carries additional server information.network: Connection forcibly cut →FetchErrorwith nostatusCode.
模块只产出 ApiError(业务错误)一种自定义错误,其余均为 ofetch 原生 FetchError;通过 isBusinessError 与 statusCode 区分场景。
<script setup lang="ts">
import type { ApiError } from '@movk/nuxt'
import type { FetchError } from 'ofetch'
const props = defineProps<{
mode: 'business' | 'http-400' | 'http-422' | 'http-500' | 'network'
}>()
const urlMap: Record<typeof props.mode, string> = {
'business': '/demo/errors?type=business',
'http-400': '/profile?fail=1',
'http-422': '/demo/errors?type=422',
'http-500': '/demo/errors?type=500',
'network': '/demo/errors?type=network'
}
const { error, execute } = useApiFetch(() => urlMap[props.mode], {
immediate: false,
toast: false
})
const info = computed(() => {
const err = error.value
if (!err) return null
const apiErr = err as Partial<ApiError>
const fetchErr = err as FetchError
return {
kind: apiErr.isBusinessError ? 'ApiError(业务错误)' : 'FetchError',
statusCode: apiErr.statusCode ?? fetchErr.statusCode ?? null,
message: err.message,
isBusinessError: apiErr.isBusinessError ?? false,
data: fetchErr.data ?? null
}
})
</script>
<template>
<div class="flex flex-col gap-3">
<UButton size="sm" color="error" variant="outline" icon="i-lucide-circle-alert" @click="execute()">
触发 {{ mode }}
</UButton>
<p class="text-xs text-muted">
模块只产出 <code>ApiError</code>(业务错误)一种自定义错误,其余均为 ofetch 原生 <code>FetchError</code>;通过 <code>isBusinessError</code> 与 <code>statusCode</code> 区分场景。
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ info }}</pre>
</div>
</template>
Toast Notifications
The toast option supports per-request configuration: false to disable all, { success: false } to keep only errors, successMessage / errorMessage for quick text, or a full set of Toast props (color, icon, duration, etc.):
// Disable Toast / keep error only / quick text
await useApiFetch('/users', { toast: false })
await useApiFetch('/users', { toast: { success: false } })
await useApiFetch('/users', {
toast: { successMessage: 'Created!', errorMessage: 'Failed, please retry' }
})
// Full Toast props
await useApiFetch('/users', {
toast: {
success: { title: 'Created', color: 'success', icon: 'i-lucide-circle-check' },
error: { title: 'Failed', color: 'error', duration: 5000 }
}
})
API
useApiFetch()
useApiFetch<T = unknown, DataT = T>(url: string | (() => string), options?: UseApiFetchOptions<T, DataT>): UseApiFetchReturn<DataT>
Creates an API request.
Parameters
useFetch options plus additional API integration options.API Integration Options
$api.false to disable Toast.false to disable success notifications.false to disable error notifications.success: { title: '...' }.error: { title: '...' }.ApiError is thrown), but the dataKey field is still unwrapped. Defaults to false.true, returns the full code/message/data envelope. Orthogonal to skipBusinessCheck. Defaults to false.Request Options (inherited from useFetch)
'GET'.query.endpoint option; no need to set manually.Response Handling
Execution Options
false to call execute() manually. Defaults to true.false.true.watch objects. Defaults to true.Caching and Deduplication
cancel.'cancel'— Cancel the pending request'defer'— Do not initiate a new request
data.Request Hooks
onRequest({ request, options }) {
console.log('Sending request:', request.url)
}
onRequestError({ request, error }) {
console.error('Request error:', error)
}
response._data is already the unwrapped business data (unless skipUnwrap: true); only fires on business success. Return values are ignored by ofetch — use transform for output transformations.onResponse({ response }) {
console.log('Unwrapped data:', response._data)
}
onResponseError({ response }) {
console.error('Response error:', response.status)
}
Type Parameters
$api). This is the data type of the data field in the API response.transform conversion. Defaults to T.Returns
Returns the response object of useFetch, including:
null; updated to the actual data after a successful request.FetchError; business status code errors are ApiError (with statusCode, response and isBusinessError properties).'idle'— Not yet started (only whenimmediate: false)'pending'— Request in progress'success'— Request succeeded'error'— Request failed
status.value === 'pending'.execute.// Refresh data
await refresh()
// Skip deduplication check and force refresh
await refresh({ dedupe: false })
refresh. Manually execute the request (commonly used with immediate: false).data to null, error to null and status to 'idle'.