Hooks

View source
Extend API system behavior via Nuxt runtime hooks — global request interception, error handling and custom 401 logic.

Introduction

The Movk Nuxt API system exposes 4 extension points via Nuxt runtime hooks, allowing you to extend and customize API behavior without modifying source code.

All hook types are automatically registered into RuntimeNuxtHooks via module declaration; in your editor, nuxtApp.hook('movk:api:') provides full auto-completion.

Available Hooks

movk:api:request

When triggered: After auth token injection, before the request is actually sent.

'movk:api:request': (context: FetchContext) => void | Promise<void>

Typical use cases: Adding extra request headers, request logging, modifying request parameters.

plugins/api-logger.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('movk:api:request', (context) => {
    console.log(`[API] ${context.options.method || 'GET'} ${context.request}`)
  })
})

movk:api:response

When triggered: After business status code validation passes and data unwrapping is complete.

'movk:api:response': (
  context: FetchContext & { response: FetchResponse<any> }
) => void | Promise<void>

At this point, context.response._data is already the unwrapped business data.

Typical use cases: Global data transformation, response logging, performance analysis.

plugins/api-analytics.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('movk:api:response', (context) => {
    analytics.track('api_success', {
      url: context.request,
      method: context.options.method || 'GET'
    })
  })
})

movk:api:error

When triggered: On business status code validation failure or HTTP error (4xx/5xx). For 401 errors, this fires after movk:api:unauthorized.

'movk:api:error': (
  context: FetchContext & { response: FetchResponse<any> }
) => void | Promise<void>

Typical use cases: Global error reporting (Sentry, etc.), error analytics.

plugins/error-reporter.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('movk:api:error', (context) => {
    // Report to Sentry (skip already-handled 401s)
    if (context.response.status !== 401) {
      Sentry.captureException(new Error(`API Error: ${context.response.status}`), {
        extra: {
          url: context.request,
          data: context.response._data
        }
      })
    }
  })
})

movk:api:unauthorized

When triggered: When an HTTP 401 response is received, before movk:api:error.

'movk:api:unauthorized': (
  context: FetchContext & { response: FetchResponse<any> },
  result: { handled: boolean }
) => void | Promise<void>

Setting result.handled = true skips the default 401 handling (clearing the session and redirecting to the login page).

If multiple handlers listen to this hook, any handler setting result.handled = true will prevent the default behavior.

Token Refresh Pattern

plugins/token-refresh.ts
export default defineNuxtPlugin((nuxtApp) => {
  let isRefreshing = false

  nuxtApp.hook('movk:api:unauthorized', async (_context, result) => {
    if (isRefreshing) return

    isRefreshing = true
    try {
      // Attempt to refresh the token
      await $fetch('/api/auth/refresh', { method: 'POST' })
      // Refresh session state
      const { fetch: fetchSession } = useUserSession()
      await fetchSession()
      // Mark as handled, skip default 401 behavior
      result.handled = true
    }
    catch {
      // Refresh failed — fall through to default behavior (clear session + redirect to login)
    }
    finally {
      isRefreshing = false
    }
  })
})

Registration

Suitable for global logic that is automatically registered at app startup:

plugins/api-hooks.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('movk:api:request', (context) => {
    // Global request handling
  })

  nuxtApp.hook('movk:api:error', (context) => {
    // Global error handling
  })
})

In a Component

Suitable for page-scoped logic:

<script setup>
const nuxtApp = useNuxtApp()

// Only active on the current page
const unregister = nuxtApp.hook('movk:api:error', (context) => {
  // Page-level error handling
})

onUnmounted(() => {
  unregister() // Clean up when the component is destroyed
})
</script>
When registering hooks inside a component, always call the returned unregister function in onUnmounted to avoid memory leaks.

Execution Order

Request initiated
  │
  ├── Auth token injection
  ├── Debug logging
  ├── movk:api:request             <-- user extension point
  │
  ├── Network request sent ─────────────>
  │
  ├── HTTP 2xx response
  │   ├── Business success (code in successCodes)
  │   │   ├── Data unwrap (extract dataKey)
  │   │   ├── movk:api:response    <-- user extension point
  │   │   └── Success Toast
  │   │
  │   └── Business failure
  │       ├── movk:api:error       <-- user extension point
  │       ├── Error Toast
  │       └── throw ApiError
  │
  └── HTTP 4xx/5xx response
      ├── [401] movk:api:unauthorized  <-- user extension point
      │       └── (if not handled) clear session + redirect to login
      ├── movk:api:error           <-- user extension point
      └── Error Toast

TypeScript Support

Hook types are automatically registered via module declaration:

src/runtime/types/module.ts
declare module 'nuxt/app' {
  interface RuntimeNuxtHooks {
    'movk:api:request': (context: FetchContext) => void | Promise<void>
    'movk:api:response': (context: FetchContext & { response: FetchResponse<any> }) => void | Promise<void>
    'movk:api:error': (context: FetchContext & { response: FetchResponse<any> }) => void | Promise<void>
    'movk:api:unauthorized': (
      context: FetchContext & { response: FetchResponse<any> },
      result: { handled: boolean }
    ) => void | Promise<void>
  }
}

No additional configuration is needed — nuxtApp.hook('movk:api:...') provides full type inference and auto-completion out of the box.

Copyright © 2025 - 2026 YiXuan - MIT License