---
title: "$api"
description: "由 api.factory 插件创建的增强型 $fetch 实例，支持认证注入、数据解包、业务检查和多端点切换。"
seo_title: "$api Plugin"
seo_description: "Configure the Movk Nuxt API plugin — endpoints, baseURL, auth, response parsing, business codes, toast and the $api instance with .use(name) switching."
canonical_url: "https://nuxt.mhaibaraai.cn/docs/api/api-plugin"
---
# $api

> 由 api.factory 插件创建的增强型 $fetch 实例，支持认证注入、数据解包、业务检查和多端点切换。

## 介绍

`$api` 是通过 Nuxt 插件注入的全局 `$fetch` 实例，内置完整的拦截器链（认证注入、业务状态码检查、数据解包、Toast 提示）。它是所有 API composable（`useApiFetch`、`useLazyApiFetch` 等）的底层引擎。

## 用法

```ts
const { $api } = useNuxtApp()

// GET 请求（数据已自动解包）
const users = await $api<User[]>('/users')

// POST 请求
const user = await $api<User>('/users', {
  method: 'POST',
  body: { name: 'test', email: 'test@example.com' }
})

// PUT 请求
const updated = await $api<User>(`/users/${id}`, {
  method: 'PUT',
  body: { name: 'new-name' }
})

// DELETE 请求
await $api(`/users/${id}`, { method: 'DELETE' })
```

> \[\!NOTE\]
> 
> $api
> 
>  返回的数据已经过拦截器自动解包（提取 
> 
> dataKey
> 
>  字段），泛型 
> 
> T
> 
>  直接对应业务数据类型。

## 多端点切换

使用 `$api.use(endpoint)` 切换到指定端点：

```ts
const { $api } = useNuxtApp()

// 使用默认端点
const users = await $api('/users')

// 切换到 admin 端点
const adminUsers = await $api.use('admin')('/users')

// 保存端点引用，避免重复 use()
const adminApi = $api.use('admin')
const config = await adminApi('/config')
const stats = await adminApi('/stats')
```

端点实例内部有缓存机制，多次调用 `$api.use('admin')` 返回同一实例。

### 切换并对照 baseURL

`$api` 默认走 default 端点的 `baseURL`，`$api.use(name)` 切换到别名端点后请求改用对应 `baseURL`。下拉切换端点即可观察 `baseURL` 与返回随之变化：

```vue [ApiEndpointsExample.vue]
<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>
```

### 命令式调用

脱离 `useFetch` 上下文，直接通过 `$api.use(name)` 取得端点实例并发起裸请求：

```vue [ApiEndpointsImperativeExample.vue]
<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>
```

### 当前生效配置

`runtimeConfig.public.movkApi` 暴露端点列表与默认端点名，便于运行时调试：

```vue [ApiEndpointsRuntimeExample.vue]
<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>
```

> \[\!TIP\]
> See: /docs/getting-started/configuration#endpoints
> 
> 了解如何配置多个 API 端点

## 认证集成

模块自动接入 `nuxt-auth-utils`：登录后从 `session[sessionTokenPath]` 读取 token，所有 `$api` 请求自动注入 `Authorization` 头；遇到 401 时按 `auth.unauthorized` 策略清理 session 或跳转登录页。

### 当前 Session

`useUserSession` 暴露的响应式 session；登录 / 登出后调用 `fetch()` 同步状态：

```vue [ApiAuthSessionExample.vue]
<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>
```

### 调用受保护接口

`useApiFetch` 自动携带 Authorization 头访问 `/demo/protected`；未登录时返回 401：

```vue [ApiAuthProtectedExample.vue]
<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 处理策略

`auth.unauthorized` 控制重定向与 session 清理；docs 站点为便于演示关闭重定向：

```vue [ApiAuthUnauthorizedExample.vue]
<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>
```

### 令牌注入格式

`headerName` \+ `tokenType` 决定最终请求头形态；后端 echo `authHeader` 字段供核对：

```vue [ApiAuthTokenInjectionExample.vue]
<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>
```

## 数据解包与业务校验

`$api` 默认按 `response.dataKey` 抽取业务数据并按 `successCodes` 校验业务码；两个开关 `skipUnwrap` / `skipBusinessCheck` 让调用方按场景接管。下拉切换模式观察返回结构差异：

- `default`：按默认 `data` 字段抽取业务数据，调用方直接拿到去信封后的对象。
- `skip-unwrap`：`skipUnwrap: true` 不重写 `response._data`，保留完整 `code/message/data` 信封。
- `skip-business-check`：legacy 接口未返回 `code` 字段，跳过校验后仍按 `dataKey` 解包出业务数据。
- `combined`：外部接口信封非标，同时开两开关由调用方完全自处理响应。

```vue [ApiUnwrapExample.vue]
<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 提示

`$api` 内置 Toast 提示，按 `toast.successCodes` 判定成功 / 失败；支持全局默认 + 请求级覆盖两层定制。下拉切换模式、配合「注入错误」开关观察成功 / 失败两条分支：

- `default`：未传 `toast` 时继承全局配置，成功 / 错误均按默认样式弹出。
- `silent`：`toast: false` 关闭所有单次提示。
- `message`：`successMessage` / `errorMessage` 替换默认文案。
- `error-only`：`{ success: false }` 关闭成功提示但保留错误提示。
- `custom`：成功 / 错误分别覆盖 `color`、`icon` 等 ToastProps。

```vue [ApiToastExample.vue]
<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>
```

## 在事件处理器中使用

事件处理器中使用 `$api` 是最常见的场景：

```vue
<script setup lang="ts">
const { $api } = useNuxtApp()

// 表单提交
async function handleCreate(formData: CreateUserRequest) {
  try {
    const user = await $api<User>('/users', {
      method: 'POST',
      body: formData
    })
    // Toast 已自动显示，无需手动处理
    navigateTo(`/users/${user.id}`)
  }
  catch (error) {
    // 业务错误 Toast 已自动显示
    // 此处可做额外逻辑（如表单字段高亮）
  }
}

// 删除确认
async function handleDelete(id: string) {
  await $api(`/users/${id}`, { method: 'DELETE' })
  await refreshNuxtData() // 刷新页面缓存数据
}
</script>
```

## 在 useAsyncData 中使用

当需要聚合多个接口或做复杂数据处理时，在 `useAsyncData` 中使用 `$api`：

```ts
const { $api } = useNuxtApp()

// 聚合多个接口
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 }
})
```

### 封装自定义 composable

```ts [app/composables/useDashboard.ts]
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 }
  })
}
```

### 业务操作 composable

对于删除、审批等写操作，将 `$api` 调用封装到独立的 composable 中，保持页面文件精简：

```ts [app/composables/useUserActions.ts]
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: '角色已更新' } },
    })
  }

  return { remove, create, updateRole }
}
```

```vue [app/pages/users/index.vue]
<script setup lang="ts">
// 数据获取用 useApiFetch
const { data: users, refresh } = await useApiFetch<User[]>('/users')

// 写操作用 $api composable
const { remove, updateRole } = useUserActions()
</script>
```

## 请求级配置

通过 `context` 字段传递请求级配置：

```ts
const { $api } = useNuxtApp()

// 禁用 Toast
await $api('/users', {
  context: { toast: false }
})

// 自定义 Toast 消息
await $api('/users', {
  method: 'POST',
  body: formData,
  context: {
    toast: { successMessage: '创建成功' }
  }
})

// 全局关闭成功提示时，单次请求开启
await $api('/users', {
  context: { toast: { success: { show: true } } }
})

// 跳过业务状态码检查（直接返回原始解包数据）
const raw = await $api('/external-api/data', {
  context: { skipBusinessCheck: true }
})
```

> \[\!NOTE\]
> 
> context
> 
>  字段通过 ofetch 的 
> 
> FetchOptions
> 
>  扩展传递，仅 Movk Nuxt 的 
> 
> $api
> 
>  实例识别此字段。原生 
> 
> $fetch
> 
>  不会处理这些选项。

## API

### ApiInstance

`$api` 的类型定义：

```ts
type ApiInstance = $Fetch & {
  use: (endpoint: string) => ApiInstance
}
```

**$api(url, options)** (`Promise<T>`): 发送 API 请求。支持所有 ofetch 的选项，额外支持 context 字段。

**$api.use(endpoint)** (`ApiInstance`): 切换到指定端点，返回该端点的 $api 实例。endpoint 类型为 MovkApiEndpointName（由模块按 movk.api.endpoints 声明合并出的字面量联合）。端点实例内部有缓存，多次调用返回同一实例。

### ApiFetchContext

通过 `options.context` 传递的扩展配置：

**toast** (`RequestToastOptions | false`): Toast 提示配置。设为 false 禁用所有 Toast；{ success: { show: true } } 可覆盖全局关闭单次开启。

**skipBusinessCheck** (`boolean`): 跳过业务状态码检查。设为 true 时不抛出 ApiError，但仍会解包 dataKey 字段。

**skipUnwrap** (`boolean`): 跳过数据解包。设为 true 时拦截器不重写 response.\_data，调用方拿到 code/message/data 完整信封。与 skipBusinessCheck 正交。


## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
