$api
Introduction
$api is a global $fetch instance injected by a Nuxt plugin, with a full built-in interceptor chain (auth injection, business status code checking, data unwrapping, Toast notifications). It is the underlying engine for all API composables (useApiFetch, useLazyApiFetch, etc.).
Usage
const { $api } = useNuxtApp()
// GET request (data is automatically unwrapped)
const users = await $api<User[]>('/users')
// POST request
const user = await $api<User>('/users', {
method: 'POST',
body: { name: 'test', email: 'test@example.com' }
})
// PUT request
const updated = await $api<User>(`/users/${id}`, {
method: 'PUT',
body: { name: 'new-name' }
})
// DELETE request
await $api(`/users/${id}`, { method: 'DELETE' })
$api has already been automatically unwrapped by the interceptor (the dataKey field is extracted); the generic T directly maps to the business data type.Multi-Endpoint Switching
Use $api.use(endpoint) to switch to a specific endpoint:
const { $api } = useNuxtApp()
// Use the default endpoint
const users = await $api('/users')
// Switch to the admin endpoint
const adminUsers = await $api.use('admin')('/users')
// Save an endpoint reference to avoid repeated use() calls
const adminApi = $api.use('admin')
const config = await adminApi('/config')
const stats = await adminApi('/stats')
Endpoint instances are internally cached, so multiple calls to $api.use('admin') return the same instance.
Switch and Compare baseURL
$api uses the baseURL of the default endpoint; calling $api.use(name) switches to the named endpoint's baseURL. Select a different endpoint in the dropdown to observe the baseURL and response change accordingly:
$api 默认走 default 端点的 baseURL,$api.use(name) 切换到别名端点后请求改用对应 baseURL。
<script setup lang="ts">
const props = defineProps<{
endpoint: 'default' | 'v2'
}>()
const { $api } = useNuxtApp()
const publicConfig = useRuntimeConfig().public.movkApi
const data = ref<unknown>(null)
const pending = ref(false)
const error = ref<string | null>(null)
const baseURL = computed(() => publicConfig.endpoints?.[props.endpoint]?.baseURL)
async function load() {
pending.value = true
error.value = null
try {
data.value = await $api.use(props.endpoint)('/profile')
}
catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
finally {
pending.value = false
}
}
watch(() => props.endpoint, load, { immediate: true })
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2 flex-wrap">
<UBadge :color="endpoint === 'v2' ? 'secondary' : 'neutral'" variant="subtle">
baseURL: {{ baseURL }}
</UBadge>
<UButton size="sm" variant="outline" icon="i-lucide-refresh-cw" :loading="pending" @click="load">
刷新
</UButton>
</div>
<p class="text-xs text-muted">
$api 默认走 default 端点的 baseURL,$api.use(name) 切换到别名端点后请求改用对应 baseURL。
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ error ? { error } : data }}</pre>
</div>
</template>
Imperative Usage
Outside a useFetch context, obtain an endpoint instance via $api.use(name) and make raw requests directly:
<script setup lang="ts">
const { $api } = useNuxtApp()
const data = ref<unknown>(null)
const pending = ref(false)
const error = ref<string | null>(null)
async function call() {
pending.value = true
error.value = null
try {
data.value = await $api.use('v2')('/profile')
}
catch (err) {
error.value = err instanceof Error ? err.message : String(err)
}
finally {
pending.value = false
}
}
</script>
<template>
<div class="flex flex-col gap-3">
<UButton size="sm" icon="i-lucide-zap" :loading="pending" @click="call">
$api.use('v2')('/profile')
</UButton>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ error ? { error } : data }}</pre>
</div>
</template>
Active Runtime Config
runtimeConfig.public.movkApi exposes the endpoint list and default endpoint name for runtime debugging:
在 nuxt.config 的 movk.api.endpoints 注册新别名后即可被 endpoint 选项引用。
{
"defaultEndpoint": "default",
"endpoints": {
"default": {
"baseURL": "/api"
},
"v2": {
"baseURL": "/api/demo/v2"
}
}
}<script setup lang="ts">
const publicConfig = useRuntimeConfig().public.movkApi
</script>
<template>
<div class="flex flex-col gap-3">
<p class="text-sm text-muted">
在 <code>nuxt.config</code> 的 <code>movk.api.endpoints</code> 注册新别名后即可被 <code>endpoint</code> 选项引用。
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ { defaultEndpoint: publicConfig.defaultEndpoint, endpoints: publicConfig.endpoints } }}</pre>
</div>
</template>
Auth Integration
The module automatically integrates with nuxt-auth-utils: after login it reads the token from session[sessionTokenPath] and all $api requests automatically inject the Authorization header. On a 401 response, the auth.unauthorized strategy clears the session or redirects to the login page.
Current Session
The reactive session exposed by useUserSession; call fetch() after login/logout to sync state:
sessionTokenPath: token
{
"id": "776e9a74-6c83-4a37-b09f-106bc27ae76e"
}<script setup lang="ts">
const { session, loggedIn, fetch: refreshSession } = useUserSession()
const { $api } = useNuxtApp()
const publicConfig = useRuntimeConfig().public.movkApi
const loginPending = ref(false)
const logoutPending = ref(false)
async function login() {
loginPending.value = true
try {
await $api('/demo/login', { method: 'POST' })
await refreshSession()
}
finally {
loginPending.value = false
}
}
async function logout() {
logoutPending.value = true
try {
await $api('/demo/logout', { method: 'POST' })
await refreshSession()
}
finally {
logoutPending.value = false
}
}
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2 flex-wrap">
<UBadge :color="loggedIn ? 'success' : 'neutral'" variant="subtle">
{{ loggedIn ? '已登录' : '未登录' }}
</UBadge>
<UButton
v-if="!loggedIn"
size="sm"
:loading="loginPending"
icon="i-lucide-log-in"
@click="login"
>
登录(写入 session.token)
</UButton>
<UButton
v-else
size="sm"
color="error"
variant="outline"
:loading="logoutPending"
icon="i-lucide-log-out"
@click="logout"
>
登出(清除 session)
</UButton>
</div>
<p class="text-xs text-muted">
sessionTokenPath: {{ publicConfig.auth?.sessionTokenPath }}
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ session }}</pre>
</div>
</template>
Calling a Protected Endpoint
useApiFetch automatically carries the Authorization header when accessing /demo/protected; returns 401 when not logged in:
未登录时拦截器返回 401;已登录则携带 Authorization 头
<script setup lang="ts">
const protectedRes = useApiFetch('/demo/protected', { immediate: false })
const display = computed(() => {
if (protectedRes.error.value) {
return {
statusCode: protectedRes.error.value.statusCode,
message: protectedRes.error.value.message
}
}
return protectedRes.data.value
})
</script>
<template>
<div class="flex flex-col gap-3">
<UButton size="sm" icon="i-lucide-shield-check" @click="protectedRes.execute()">
请求 /demo/protected
</UButton>
<p class="text-xs text-muted">
未登录时拦截器返回 401;已登录则携带 Authorization 头
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ display }}</pre>
</div>
</template>
401 Handling Strategy
auth.unauthorized controls redirect and session clearing; this docs site disables redirect for demonstration purposes:
auth.unauthorized 控制 401 时的行为;docs 站点关闭重定向便于演示,生产建议启用 redirect: true + loginPath。
{
"redirect": false,
"clearSession": true,
"loginPath": "/login"
}<script setup lang="ts">
const publicConfig = useRuntimeConfig().public.movkApi
</script>
<template>
<div class="flex flex-col gap-3">
<p class="text-sm text-muted">
<code>auth.unauthorized</code> 控制 401 时的行为;docs 站点关闭重定向便于演示,生产建议启用 <code>redirect: true</code> + <code>loginPath</code>。
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ publicConfig.auth?.unauthorized }}</pre>
</div>
</template>
Token Injection Format
headerName + tokenType determine the final request header format; the backend echoes the authHeader field for verification:
{
"headerName": "Authorization",
"tokenType": "Bearer",
"echoed": null
} 登录后再次请求,echoed 字段会展示拦截器实际注入的 Authorization 头。
<script setup lang="ts">
const publicConfig = useRuntimeConfig().public.movkApi
const protectedRes = useApiFetch('/demo/protected', { immediate: false })
const echoed = computed(() => {
const data = protectedRes.data.value
return data && typeof data === 'object'
? (data as { authHeader?: string }).authHeader
: null
})
</script>
<template>
<div class="flex flex-col gap-3">
<UButton size="sm" variant="outline" icon="i-lucide-send" @click="protectedRes.execute()">
请求 /demo/protected 查看注入
</UButton>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ {
headerName: publicConfig.auth?.headerName ?? 'Authorization',
tokenType: publicConfig.auth?.tokenType ?? 'Bearer',
echoed
} }}</pre>
<p class="text-xs text-muted">
登录后再次请求,<code>echoed</code> 字段会展示拦截器实际注入的 Authorization 头。
</p>
</div>
</template>
Data Unwrapping and Business Validation
$api extracts business data by response.dataKey and validates the business code against successCodes by default. The two switches skipUnwrap / skipBusinessCheck let the caller take over per scenario. Select a mode in the dropdown to observe the difference in the returned structure:
default: Extracts business data by the defaultdatafield; the caller receives the de-enveloped object directly.skip-unwrap:skipUnwrap: true— does not overwriteresponse._data, preserving the fullcode/message/dataenvelope.skip-business-check: Legacy endpoints that don't return acodefield — skip validation and still unwrap the business data bydataKey.combined: Non-standard external envelopes — enable both switches to let the caller handle the response entirely.
按 dataKey(默认 data)抽取业务数据,调用方直接拿到去信封后的对象。
<script setup lang="ts">
const props = defineProps<{
mode: 'default' | 'skip-unwrap' | 'skip-business-check' | 'combined'
}>()
const fetchers = {
'default': useApiFetch('/profile', { key: 'unwrap-default', immediate: false }),
'skip-unwrap': useApiFetch('/profile', { key: 'unwrap-skip-unwrap', immediate: false, skipUnwrap: true }),
'skip-business-check': useApiFetch('/demo/legacy', { key: 'unwrap-skip-business-check', immediate: false, skipBusinessCheck: true }),
'combined': useApiFetch('/demo/external', { key: 'unwrap-combined', immediate: false, skipBusinessCheck: true, skipUnwrap: true })
}
const active = computed(() => fetchers[props.mode])
const hints: Record<typeof props.mode, string> = {
'default': '按 dataKey(默认 data)抽取业务数据,调用方直接拿到去信封后的对象。',
'skip-unwrap': 'skipUnwrap: true 不重写 response._data,保留完整 code/message/data 信封。',
'skip-business-check': 'legacy 接口未返回 code 字段,跳过校验后仍按 dataKey 解包出业务数据。',
'combined': 'external 接口信封非标,同时开两开关由调用方完全自处理响应。'
}
watch(() => props.mode, () => active.value.execute(), { immediate: true })
</script>
<template>
<div class="flex flex-col gap-3">
<UBadge variant="subtle">
{{ mode }}
</UBadge>
<p class="text-xs text-muted">
{{ hints[mode] }}
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ active.data.value }}</pre>
</div>
</template>
Toast Notifications
$api has built-in Toast notifications, determining success/failure by toast.successCodes. Supports two levels of customization: global defaults and per-request overrides. Select a mode in the dropdown and toggle the "inject error" switch to observe the success/failure branches:
default: Notoastpassed — inherits global config; both success and error use the default style.silent:toast: false— disables all per-request notifications.message:successMessage/errorMessagereplace the default text.error-only:{ success: false }— disables success notification but keeps error notification.custom: Overridescolor,iconand other ToastProps for success and error separately.
未传 toast 时继承全局配置,成功 / 错误均按默认样式弹出。
<script setup lang="ts">
const props = defineProps<{
mode: 'default' | 'silent' | 'message' | 'error-only' | 'custom'
}>()
const fail = ref(false)
const target = () => `/profile${fail.value ? '?fail=1' : ''}`
const fetchers = {
'default': useApiFetch(target, { key: 'toast-default', immediate: false }),
'silent': useApiFetch(target, { key: 'toast-silent', immediate: false, toast: false }),
'message': useApiFetch(target, {
key: 'toast-message',
immediate: false,
toast: { successMessage: '加载成功 ✓', errorMessage: '加载失败 ✗' }
}),
'error-only': useApiFetch(target, { key: 'toast-error-only', immediate: false, toast: { success: false } }),
'custom': useApiFetch(target, {
key: 'toast-custom',
immediate: false,
toast: {
success: { color: 'secondary', icon: 'i-lucide-sparkles' },
error: { color: 'warning', icon: 'i-lucide-triangle-alert' }
}
})
}
const active = computed(() => fetchers[props.mode])
const hints: Record<typeof props.mode, string> = {
'default': '未传 toast 时继承全局配置,成功 / 错误均按默认样式弹出。',
'silent': 'toast: false 关闭所有单次提示。',
'message': 'successMessage / errorMessage 替换默认文案。',
'error-only': '{ success: false } 关闭成功提示但保留错误提示。',
'custom': '为成功 / 错误分别覆盖 color、icon 等 ToastProps。'
}
const display = computed(() => active.value.error.value
? { error: active.value.error.value.message }
: active.value.data.value)
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3 flex-wrap">
<USwitch v-model="fail" label="注入错误(验证错误分支)" />
<UButton size="sm" variant="outline" icon="i-lucide-send" @click="active.execute()">
触发请求
</UButton>
</div>
<p class="text-xs text-muted">
{{ hints[mode] }}
</p>
<pre class="text-xs p-3 rounded bg-elevated overflow-auto">{{ display }}</pre>
</div>
</template>
Using in Event Handlers
Using $api inside event handlers is the most common scenario:
<script setup lang="ts">
const { $api } = useNuxtApp()
// Form submission
async function handleCreate(formData: CreateUserRequest) {
try {
const user = await $api<User>('/users', {
method: 'POST',
body: formData
})
// Toast is shown automatically, no manual handling needed
navigateTo(`/users/${user.id}`)
}
catch (error) {
// Business error Toast is shown automatically
// Add extra logic here if needed (e.g. highlight form fields)
}
}
// Delete confirmation
async function handleDelete(id: string) {
await $api(`/users/${id}`, { method: 'DELETE' })
await refreshNuxtData() // Refresh page-cached data
}
</script>
Using in useAsyncData
When you need to aggregate multiple endpoints or do complex data processing, use $api inside useAsyncData:
const { $api } = useNuxtApp()
// Aggregate multiple endpoints
const { data: dashboard } = await useAsyncData('dashboard', async () => {
const [users, stats, recent] = await Promise.all([
$api<User[]>('/users'),
$api<Stats>('/stats'),
$api<Activity[]>('/activity/recent')
])
return { users, stats, recent }
})
Wrapping a Custom Composable
export function useDashboard() {
const { $api } = useNuxtApp()
return useAsyncData('dashboard', async () => {
const [users, stats] = await Promise.all([
$api<User[]>('/users'),
$api<Stats>('/stats')
])
return { users, stats }
})
}
Business Operation Composable
For write operations such as delete or approve, encapsulate the $api calls in a dedicated composable to keep page files lean:
export function useUserActions() {
const { $api } = useNuxtApp()
async function remove(id: string) {
await $api(`/users/${id}`, { method: 'DELETE' })
await refreshNuxtData()
}
async function create(data: CreateUserRequest) {
return await $api<User>('/users', {
method: 'POST',
body: data,
})
}
async function updateRole(id: string, role: UserRole) {
return await $api<User>(`/users/${id}/role`, {
method: 'PUT',
body: { role },
context: { toast: { successMessage: 'Role updated' } },
})
}
return { remove, create, updateRole }
}
<script setup lang="ts">
// Data fetching with useApiFetch
const { data: users, refresh } = await useApiFetch<User[]>('/users')
// Write operations with $api composable
const { remove, updateRole } = useUserActions()
</script>
Per-Request Configuration
Pass per-request configuration via the context field:
const { $api } = useNuxtApp()
// Disable Toast
await $api('/users', {
context: { toast: false }
})
// Custom Toast message
await $api('/users', {
method: 'POST',
body: formData,
context: {
toast: { successMessage: 'Created successfully' }
}
})
// Skip business status code checking (returns the raw unwrapped data)
const raw = await $api('/external-api/data', {
context: { skipBusinessCheck: true }
})
context field is passed via ofetch's FetchOptions extension and is only recognized by Movk Nuxt's $api instance. The native $fetch does not process these options.API
ApiInstance
Type definition of $api:
type ApiInstance = $Fetch & {
use: (endpoint: string) => ApiInstance
}
context field.$api instance. endpoint is typed as MovkApiEndpointName (a literal union merged by the module from the movk.api.endpoints declaration). Endpoint instances are internally cached; multiple calls return the same instance.ApiFetchContext
Extended configuration passed via options.context:
false to disable all Toast notifications.true, ApiError is not thrown, but the dataKey field is still unwrapped.true, the interceptor does not overwrite response._data; the caller receives the full code/message/data envelope. Orthogonal to skipBusinessCheck.