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

Latest version **4.5.0** (published 2026-09-24) · MIT license · 0 weekly downloads

## Install

```sh
npm install nuxt-custom-fetch
pnpm add nuxt-custom-fetch
yarn add nuxt-custom-fetch
bun add nuxt-custom-fetch
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 4.5.0 |
| Published | 2026-09-24 |
| First published | 2023-01-09 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 2 |
| Unpacked size | 43.9 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 19 |
| Maintainers | xjccc |

## Links

- npm: https://www.npmjs.com/package/nuxt-custom-fetch
- Repository: https://github.com/xjccc/nuxt-custom-fetch
- Homepage: https://github.com/xjccc/nuxt-custom-fetch#readme
- Issues: https://github.com/xjccc/nuxt-custom-fetch/issues
- npm.io page: https://npm.io/package/nuxt-custom-fetch

## Dependencies (2)

- [ohash](https://npm.io/package/ohash.md) ^2.0.11
- [@vue/shared](https://npm.io/package/@vue/shared.md) ^3.5.30

## Recent versions

- 4.5.0 (latest) — 2026-09-24
- 4.1.0 (alpha) — 2026-03-13
- 4.4.8 — 2026-06-17
- 4.4.0 — 2026-04-02
- 4.1.1 — 2026-03-13
- 4.0.1 — 2025-07-16
- 4.0.0-alpha.14 — 2025-07-15
- 2.3.2 — 2025-07-07
- 4.0.0-alpha.13 — 2025-07-02
- 4.0.0-alpha.12 — 2025-06-06
- 4.0.0-alpha.11 — 2025-06-04
- 4.0.0-alpha.10 — 2025-05-30
- 4.0.0-alpha.9 — 2025-05-13
- 4.0.0-alpha.8 — 2025-05-09
- 4.0.0-alpha.7 — 2025-04-29
- … 40 more at https://npm.io/package/nuxt-custom-fetch/versions

## README

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

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

```bash
pnpm add nuxt-custom-fetch
```

Register the module in `nuxt.config.ts`:

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

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

```ts
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](playground/api/index.ts).

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

- Shared Greeting State: [playground/pages/example/v4-fetch.vue](playground/pages/example/v4-fetch.vue) and [playground/components/Test.vue](playground/components/Test.vue) use `getGreeting` to show that a page and a child component can share one keyed async-data bucket.
- Route-Driven Greeting: [playground/pages/example/v4-reactive-[id].vue](playground/pages/example/v4-reactive-%5Bid%5D.vue) uses `getGreetingByUserId` with a reactive key derived from the route.
- Reactive Page List: [playground/pages/example/reactive.vue](playground/pages/example/reactive.vue) uses `getReactivePageList` to show page-based refetching driven by a reactive page ref.
- Query Normalization: [playground/pages/example/handler.vue](playground/pages/example/handler.vue) shows how a `handler` can normalize and enrich list query parameters before the request is sent.
- Slow Metric Dedupe: [playground/pages/example/duplicate.vue](playground/pages/example/duplicate.vue) demonstrates same-key cancellation for repeated slow client requests with `dedupe: 'cancel'`.
- Manual Refresh & Clear: [playground/pages/example/test.vue](playground/pages/example/test.vue) uses `getGreeting` to compare `refresh()` and `clear()` across two independent keyed requests.

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

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

- https://nuxt.com/docs/4.x/api/composables/create-use-async-data
- https://nuxt.com/docs/4.x/api/composables/create-use-fetch
- https://nuxt.com/docs/4.x/api/composables/use-async-data
- https://nuxt.com/docs/4.x/api/composables/use-fetch
- https://nuxt.com/docs/4.x/api/utils/dollarfetch

---
_Source: https://npm.io/package/nuxt-custom-fetch · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
