---
title: "$api"
description: "The enhanced $fetch instance created by the api.factory plugin, supporting auth injection, data unwrapping, business code checking and multi-endpoint switching."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/api/api-plugin"
---
# $api

> The enhanced $fetch instance created by the api.factory plugin, supporting auth injection, data unwrapping, business code checking and multi-endpoint switching.

## 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

```ts
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' })
```

> [!NOTE]
> 
> Data returned by 
> 
> $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:

```ts
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:

```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>
```

### Imperative Usage

Outside a `useFetch` context, obtain an endpoint instance via `$api.use(name)` and make raw requests directly:

```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>
```

### Active Runtime Config

`runtimeConfig.public.movkApi` exposes the endpoint list and default endpoint name for runtime debugging:

```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
> 
> Learn how to configure multiple API endpoints

## 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:

```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>
```

### Calling a Protected Endpoint

`useApiFetch` automatically carries the Authorization header when accessing `/demo/protected`; returns 401 when not logged in:

```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 Handling Strategy

`auth.unauthorized` controls redirect and session clearing; this docs site disables redirect for demonstration purposes:

```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>
```

### Token Injection Format

`headerName` + `tokenType` determine the final request header format; the backend echoes the `authHeader` field for verification:

```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>
```

## 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 default `data` field; the caller receives the de-enveloped object directly.
- `skip-unwrap`: `skipUnwrap: true` — does not overwrite `response._data`, preserving the full `code/message/data` envelope.
- `skip-business-check`: Legacy endpoints that don't return a `code` field — skip validation and still unwrap the business data by `dataKey`.
- `combined`: Non-standard external envelopes — enable both switches to let the caller handle the response entirely.

```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 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`: No `toast` passed — inherits global config; both success and error use the default style.
- `silent`: `toast: false` — disables all per-request notifications.
- `message`: `successMessage` / `errorMessage` replace the default text.
- `error-only`: `{ success: false }` — disables success notification but keeps error notification.
- `custom`: Overrides `color`, `icon` and other ToastProps for success and error separately.

> [!NOTE]
> 
> Setting 
> 
> toast.success.methods
> 
>  in the global or endpoint config limits success notifications to mutating requests (POST / PUT / PATCH / DELETE). A single GET can still opt back in with request-level 
> 
> { success: { show: true } }
> 
>  or 
> 
> successMessage
> 
> .

```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>
```

## Using in Event Handlers

Using `$api` inside event handlers is the most common scenario:

```vue
<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`:

```ts
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

```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 }
  })
}
```

### Business Operation Composable

For write operations such as delete or approve, encapsulate the `$api` calls in a dedicated composable to keep page files lean:

```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: 'Role updated' } },
    })
  }

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

```vue [app/pages/users/index.vue]
<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:

```ts
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 }
})
```

> [!NOTE]
> 
> The 
> 
> 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`:

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

**$api(url, options)** (`Promise<T>`): Send an API request. Supports all ofetch options plus the additional context field.

**$api.use(endpoint)** (`ApiInstance`): Switch to the specified endpoint and return that endpoint's $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`:

**toast** (`RequestToastOptions | false`): Toast configuration. Set to false to disable all Toast notifications.

**skipBusinessCheck** (`boolean`): Skip business status code checking. When true, ApiError is not thrown, but the dataKey field is still unwrapped.

**skipUnwrap** (`boolean`): Skip data unwrapping. When true, the interceptor does not overwrite response._data; the caller receives the full code/message/data envelope. Orthogonal to skipBusinessCheck.


## Sitemap

See the full [sitemap](https://nuxt.mhaibaraai.cn/sitemap.md) for all pages.
