useApiFetch

View source
An API request composable built on Nuxt useFetch, providing automatic authentication, business status code checking and Toast notifications.

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>
  • useApiFetch inherits all features of Nuxt useFetch while providing additional API integration.
  • Automatically retrieves the token from the session and adds it to request headers (via the $api instance).
  • 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:

ComposableExecutionBlocks NavigationUse Case
useApiFetchSSR + CSRYesAbove-the-fold core data
useLazyApiFetchSSR + CSRNoSecondary / non-above-the-fold data
useClientApiFetchCSR onlyNoNon-SEO-sensitive data

useApiFetch fires the request during SSR, delivering data on first render:

ready
{
  "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 }))
})
See the $api page for interactive examples of the unwrapping mechanism and skipUnwrap / skipBusinessCheck

Business 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
})
See interactive examples for business validation and skipBusinessCheck on the $api page

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 but code not in successCodesApiError, isBusinessError is true.
  • http-400 / http-500: Native FetchError with statusCode set to the corresponding code.
  • http-422: FetchError whose data field carries additional server information.
  • network: Connection forcibly cut → FetchError with no statusCode.

模块只产出 ApiError(业务错误)一种自定义错误,其余均为 ofetch 原生 FetchError;通过 isBusinessErrorstatusCode 区分场景。

<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 }
  }
})
See interactive examples for five Toast modes on the $api page

API

useApiFetch()

useApiFetch<T = unknown, DataT = T>(url: string | (() => string), options?: UseApiFetchOptions<T, DataT>): UseApiFetchReturn<DataT>

Creates an API request.

Parameters

url
string | (() => string) required
Request URL or a function returning the URL. Supports reactive URLs.
options
UseApiFetchOptions<T, DataT>
Request configuration options. Supports all Nuxt useFetch options plus additional API integration options.

Type Parameters

T
type
Business data type (already automatically unwrapped by $api). This is the data type of the data field in the API response.
DataT
type
The final type after transform conversion. Defaults to T.

Returns

Returns the response object of useFetch, including:

data
Ref<DataT | null>
Response data (unwrapped and transformed). Initially null; updated to the actual data after a successful request.
error
Ref<FetchError | ApiError | null>
Error object. Network errors are FetchError; business status code errors are ApiError (with statusCode, response and isBusinessError properties).
status
Ref<'idle' | 'pending' | 'success' | 'error'>
Request status.
  • 'idle' — Not yet started (only when immediate: false)
  • 'pending' — Request in progress
  • 'success' — Request succeeded
  • 'error' — Request failed
pending
Ref<boolean>
Whether a request is in progress. Equivalent to status.value === 'pending'.
refresh
(opts?: { dedupe?: boolean }) => Promise<void>
Refresh data by re-executing the request. Alias: execute.
execute
(opts?: { dedupe?: boolean }) => Promise<void>
Alias for refresh. Manually execute the request (commonly used with immediate: false).
clear
() => void
Clear state: sets data to null, error to null and status to 'idle'.
Copyright © 2025 - 2026 YiXuan - MIT License