npm.io
4.5.0 • Published 13h ago

nuxt-custom-fetch

Licence
MIT
Version
4.5.0
Deps
2
Size
44 kB
Vulns
0
Weekly
0
Stars
19

Nuxt Custom Fetch

nuxt-custom-fetch is a Nuxt 4 wrapper built on top of the official async-data primitives. It keeps a shared request layer with interceptors, deterministic key generation, param/query preprocessing, and a client compatibility fallback for calls made after mount.

For new Nuxt 4 code, start with the official APIs first:

  • useFetch
  • useAsyncData
  • createUseFetch
  • createUseAsyncData
  • $fetch

Use CustomFetch when you specifically need an extra request layer on top of those primitives, not as a replacement for them.

Compatibility

  • v4: Nuxt >= 4.5.0
  • v2: Nuxt 3.0.0 to 3.16.x

Current Maintenance Summary

The current implementation is maintained around these guarantees:

  • runtime behavior is aligned with Nuxt 4 async-data semantics where possible
  • public method generics follow Nuxt AsyncData typing more closely
  • reactive key, baseURL, params, query, headers, body, and cache values are resolved before each request
  • generated keys are derived with Nuxt's hashKey (the same digest useFetch uses) and include both params and query, which avoids stale client reuse when only one side changes
  • same-key client compatibility requests share one async-data bucket, so dedupe: 'cancel' can abort the previous pending request
  • the client compatibility path mirrors Nuxt 4 async-data behavior: pending follows the pendingWhenIdle config, refreshNuxtData() (the app:data:refresh hook) re-runs fallback requests, getCachedData is honored and successful data is written back to nuxtApp.payload.data
  • the client compatibility path honors Nuxt 4.5's enabled option (a reactive enabled turning false cancels the in-flight request) and cancels an in-flight request when its owning scope is disposed
  • timeouts and external abort signals are merged into one request signal (via AbortSignal.timeout/AbortSignal.any semantics), key watchers are only created for reactive keys, and a key change re-runs the request when immediate, prior data, or an in-flight request is present
  • Vitest runtime tests and TypeScript type tests cover the wrapper behavior

When To Use This Module

CustomFetch is useful when you want:

  • shared request and response interceptors
  • deterministic hash-based request keys
  • explicit immutableKey control
  • param or query preprocessing through handler
  • one request API that still works after mount on the client

If all you need is a typed composable with shared defaults, prefer Nuxt's official factories:

export const useAPI = createUseFetch({
  baseURL: '/api',
  lazy: true
})

export const useCachedData = createUseAsyncData({
  deep: false
})

How It Works

  • In setup (including client-side navigation), route middleware, plugins during hydration, and other setup-compatible contexts, CustomFetch delegates back to useAsyncData, so options such as lazy, server, and enabled follow Nuxt exactly.
  • After hydration on the client, calls made outside component setup (event handlers, lifecycle hooks such as onMounted, watchers) fall back to a compatibility mode. This is the same condition under which Nuxt warns "Component is already mounted".
  • The compatibility mode still exposes data, error, status, pending, refresh, execute, and clear.
  • The compatibility mode is not a full SSR payload or cache replacement.
  • Calls that intentionally share the same key should keep handler, deep, transform, pick, getCachedData, and default consistent, matching Nuxt's keyed async-data rules.

Installation

pnpm add nuxt-custom-fetch

Register the module in nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['nuxt-custom-fetch']
})

CustomFetch is auto-imported in Nuxt app code. In plain TypeScript helpers, importing it from #imports keeps editor support explicit.

Quick Start

import { CustomFetch } from '#imports'

export const ajax = new CustomFetch({
  baseURL: '/api',
  showLogs: true,
  handler: input => input,
  offline: () => {
    console.warn('Device is offline')
  }
})

export const getUsers = () =>
  ajax.get<{ data: Array<{ id: number, name: string }> }>('/users')

export const createUser = (payload: { name: string }) =>
  ajax.post<{ id: number, name: string }>('/users', {
    body: payload
  })

Always await ajax.get, ajax.post, and ajax.request in setup-compatible code.

Reactive Example

import { computed, ref } from 'vue'
import { CustomFetch } from '#imports'

const ajax = new CustomFetch({ baseURL: '/api' })

const page = ref(1)
const requestKey = computed(() => `list:${page.value}`)

const listState = await ajax.get<{
  data: number[]
  nums: number
}>('/get-list', {
  key: requestKey,
  params: { page }
}, {
  watch: [page],
  dedupe: 'cancel'
})

page.value++
await listState.refresh()

Playground Real Examples

The playground examples now use one consistent set of scenario names and helper names. The shared helper file lives in playground/api/index.ts.

export const getGreeting = (params: Record<string, unknown>) =>
  ajax.get<string>('/hello', { params })

export const getGreetingByUserId = (key: MaybeRefOrGetter<string>, { userId = 1 } = {}) =>
  ajax.get<string>('/hello', {
    key,
    params: { userId }
  }, {
    default: () => '11'
  })

export function getReactivePageList (page: Ref<number>) {
  return ajax.get<{ data: number[], nums: number }>('/get-list', {
    params: { page }
  }, {
    watch: [() => page.value]
  })
}

The remaining helper names follow the same rule: getPageList is the plain non-reactive list request, and getDelayedPageMetric is the intentionally slow metric request used for dedupe demonstrations.

Upgrading From 4.4.x

  • Nuxt >= 4.5.0 is required.
  • Calls made in setup during client-side navigation now go through useAsyncData, so lazy: true no longer blocks navigation and server/enabled follow Nuxt's rules. Calls from event handlers, onMounted, or watchers still use the compatibility mode.
  • Errors thrown by useAsyncData are no longer swallowed and turned into a fallback.
  • Requests are sent with query only; params is merged into it. Interceptors still see options.params, which ofetch mirrors from query.
  • Generated keys changed because they now use hashKey. Only code that hard-coded a generated key (for example in useNuxtData) needs updating.
  • error is typed as NuxtError<unknown> | undefined by default.

Request Semantics

Key generation and dedupe
  • Without an explicit key, the module hashes url + method + resolved request options with Nuxt's hashKey.
  • Both params and query participate in the generated key (they are merged into query before hashing).
  • FormData bodies are hashed by their ordered entries (files as name:size:lastModified) and URLSearchParams bodies by their entries, so different uploads never share a key.
  • immutableKey: true makes the generated key depend only on the URL.
  • If you need exact cache control, provide your own key.
  • Same key means shared async-data state.
  • dedupe: 'cancel' aborts the previous in-flight request inside that shared state.
  • dedupe: 'defer' reuses the existing pending request.
Reactive inputs
  • key, baseURL, params, query, headers, body, and cache can be refs, computed values, or getters.
  • Reactive inputs are deeply resolved before every request.
  • Use watch when you want a reactive source to trigger a refetch.
Handler behavior
  • handler receives a merged object built from params and query.
  • The processed output is always sent as query. params is ofetch's deprecated alias of query, so it is only read as input and never forwarded on its own.
  • Set useHandler: false on a request to bypass preprocessing; the merged object is still sent as query.
Client compatibility mode
  • Client calls made after mount reuse existing same-key async-data state when available, and still get the full AsyncData shape (refresh, execute, and clear included).
  • If no Nuxt-managed keyed state exists yet, the module creates and caches a compatibility async-data instance by key.
  • refresh, execute, clear, watch, status updates, and cancellation still work in this mode.
  • This mode should not be treated as a full SSR payload cache replacement.

Public API

const ajax = new CustomFetch({
  baseURL: '',
  immutableKey: false,
  showLogs: import.meta.dev,
  useHandler: true,
  handler: undefined,
  onRequest: undefined,
  onRequestError: undefined,
  onResponse: undefined,
  onResponseError: undefined,
  offline: undefined
})

Available methods:

  • ajax.get(url, config?, asyncDataOptions?)
  • ajax.post(url, config?, asyncDataOptions?)
  • ajax.request(url, { method, ...config }, asyncDataOptions?)

The return value follows Nuxt's AsyncData<...> shape and can use the same default, pick, transform, watch, dedupe, timeout, and enabled options you already know from useAsyncData.

showLogs defaults to true in development and prints each client request with console.info (real problems such as an unserializable body still use console.warn). Set showLogs: false to silence it.

Typing Notes

  • CustomFetch mirrors Nuxt async-data generics closely enough for default, pick, and transform to narrow the final data type.
  • error defaults to NuxtError<unknown> | undefined, like useAsyncData; the second generic (NuxtErrorDataT) types error.data.
  • Playground examples and type tests cover explicit generics, default values, and reactive arguments.
  • If you want a custom wrapper with only shared defaults, Nuxt's createUseFetch and createUseAsyncData remain the simpler choice.

Nuxt 4 API Reminders

  • useFetch is the official shortcut for useAsyncData + $fetch.
  • $fetch alone inside SSR setup will fetch twice during hydration unless it is wrapped by useAsyncData or replaced with useFetch.
  • Relative useFetch calls on the server forward headers and cookies automatically. Plain $fetch does not.
  • dedupe, timeout, watch, reactive keys, and AbortSignal are part of Nuxt's async-data contract.

Development

  • pnpm dev:prepare builds stubs and prepares the playground
  • pnpm dev starts the playground
  • pnpm dev:build builds the playground
  • pnpm test runs the Vitest suite
  • pnpm run test:coverage generates a coverage report
  • pnpm run test:types runs TypeScript type checks

References