npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nuxt-fetch-factory

v0.1.2

Published

Nuxt module providing defineFetch and defineAsyncData factory composables with per-call-site key isolation, interceptor composition, and context restoration after await.

Downloads

194

Readme

nuxt-fetch-factory

A Nuxt module that provides defineFetch and defineAsyncData factory composables. It extends Nuxt's data-fetching layer with two capabilities:

  • Per-call-site key isolation — Nuxt's scanner assigns unique payload keys to bare useFetch / useAsyncData call-sites, but cannot see through custom wrapper composables. Every component that calls the same wrapper shares a single key, so one component's cached payload leaks into another. This module replicates the same analysis for factory-created composables, injecting a unique key per call-site. On by default; disable per factory with autoKey: false when shared caching is intentional. → nuxt/nuxt#14736

  • Context restoration after await — all composables returned by this module run inside nuxtApp.runWithContext(), so they work correctly even when called after an await in a parent composable. → nuxt/nuxt#33736


How it works

1. Per-call-site key isolation — nuxt/nuxt#14736

Nuxt intentionally assigns a single deduplication key to each useFetch / useAsyncData call-site so that multiple components fetching the same resource share one request and one SSR payload — a deliberate performance optimisation.

It does this by statically scanning your source files for the bare useFetch and useAsyncData symbols. That works perfectly for direct calls, but the scanner cannot see through a custom wrapper composable — so every call-site of that wrapper shares the single key generated from the wrapper's own source location, regardless of which component is calling it or what URL is passed:

// composables/useApi.ts
export const useApi = (url: string) => useFetch(url)
//                                     └─ one call-site → one key for all usages
// PageA.vue — fetches /users, stores result under key 'useApi_abc'
const { data: users } = await useApi('/users')

// PageB.vue — key 'useApi_abc' is already populated from PageA's payload;
// Nuxt returns the cached /users data instead of fetching /posts
const { data: posts } = await useApi('/posts') // ← gets users, not posts

This module ships a Vite transform plugin that performs the same analysis for composables created with defineFetch / defineAsyncData. At build time it injects a unique _autoKey into every call-site so each gets its own isolated payload slot, exactly as if it were a direct useFetch call:

// PageA.vue
const { data: users } = await useApi('/users', { _autoKey: 'useApi_a1b2c3d4' })
// PageB.vue — different key, independent payload slot
const { data: posts } = await useApi('/posts', { _autoKey: 'useApi_e5f6a7b8' })

The key is derived from <composableName>::<relativeFilePath>::<callIndex> and is stable across rebuilds, so SSR hydration works correctly.

This behaviour is on by default and can be turned off per factory with autoKey: false — useful when you intentionally want all components to share one cached result (e.g. a global useCurrentUser singleton). See the autoKey option in the API reference.

2. Nuxt context lost after await — nuxt/nuxt#33736

Vue sets a global "current instance" pointer at the start of setup(). Any await suspends the component and Vue clears that pointer. Composables that call useNuxtApp(), useCookie(), useState(), or useFetch() internally will throw when invoked after an await in a parent composable:

A composable that requires access to the Nuxt instance was called outside
of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.

All composables returned by this module call nuxtApp.runWithContext() around their inner useFetch / useAsyncData invocation, re-establishing the context transparently. The withContext and withContextAsync utilities extend this protection to any composable you write yourself.

See Vue: composable usage restrictions and the runWithContext API docs.


Installation

npm install nuxt-fetch-factory
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-fetch-factory'],
})

Quick start

Service-layer pattern

The recommended approach is to create a factory/ directory where each file represents a service domain. Each file creates a private HTTP client with defineFetch and exports named composables for specific endpoints.

src/
  factory/
    userService.ts    →  useGetProfile, useGetPosts  (auto-imported)
    externalService.ts→  useGetFeed                  (auto-imported)
  composables/
    useProfilePage.ts →  useProfilePage              (auto-imported, Nuxt built-in dir)
  pages/
    index.vue

The factory file exports are auto-imported exactly like composables/ exports. The naming convention for exported composables follows the standard use prefix to stay consistent with Nuxt's built-in composables and avoid naming conflicts.

Set a default baseURL (optional)

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-fetch-factory'],
  fetchFactory: { baseURL: '/api' },
})

Any factory that does not set its own baseURL inherits this value. Override it per-factory for external APIs.

Define a service file

// factory/userService.ts
//
// Private API client — not exported, used only within this file.
// `baseURL` is inherited from nuxt.config.ts fetchFactory.baseURL ('/api').
const api = defineFetch({
  onRequest({ options }) {
    const token = useCookie('auth_token')
    if (token.value)
      options.headers = { ...options.headers, Authorization: `Bearer ${token.value}` }
  },
  onResponseError({ response }) {
    if (response.status === 401) navigateTo('/login')
  },
})

// Each api() call is a distinct call site — the Vite transform injects
// a different _autoKey into each, giving them isolated payload slots.
export const useGetProfile = () => api('/me')
export const useGetPosts   = () => api('/posts')
// factory/externalService.ts
//
// Overrides baseURL to point at a third-party origin.
const api = defineFetch({
  baseURL: 'https://api.example.com',
  onRequest({ options }) {
    const { public: { apiKey } } = useRuntimeConfig()
    options.headers = { ...options.headers, 'X-API-Key': apiKey }
  },
})

export const useGetFeed = (limit = 10) => api('/feed', { query: { limit } })

Use the composables in a page

// pages/index.vue — useGetProfile, useGetPosts, useGetFeed are auto-imported
const { data: profile } = await useGetProfile()
const { data: posts }   = await useGetPosts()
const { data: feed }    = await useGetFeed(5)
// All three have different _autoKey values — isolated payload slots.

API reference

defineFetch(defaults)

Creates a useFetch composable with pre-configured defaults.

Factory-level options accept everything useFetch accepts except default, transform, and pick (which are per-call concerns). The four lifecycle interceptors (onRequest, onRequestError, onResponse, onResponseError) compose with call-site interceptors — both run, factory first.

Generic type parameters:

| Parameter | Default | Description | |---|---|---| | DefaultDataT | unknown | Default response type for all calls via this factory | | — | — | Call-site generics below | | ResT | DefaultDataT | Raw response type for a single call | | TransformT | ResT | Output type after applying transform at the call site |

autoKey — opting out of per-call-site key isolation:

By default every composable produced by defineFetch gets a unique _autoKey injected per call-site, giving each component its own isolated payload slot (the fix for #14736).

Set autoKey: false when you want all call-sites to share one payload — for example a global singleton composable where every component should read from the same cache:

// factory/authService.ts
const api = defineFetch({ baseURL: '/api', autoKey: false })

// Every component calling useCurrentUser() reads the same cached slot.
// PageA, PageB, and the header component all share one fetch.
export const useCurrentUser = () => api('/me')

When autoKey: false, Nuxt falls back to its standard key strategy (URL + options hash), so cross-component deduplication still works as expected.

baseURL resolution order (lowest to highest priority):

  1. Module option fetchFactory.baseURL in nuxt.config.ts
  2. baseURL in the factory's own defineFetch({ baseURL }) options
  3. baseURL passed at the call site
// Set the default response type at the factory level
const api = defineFetch<User[]>({ baseURL: '/api/users' })

const { data } = await api('/')     // data: Ref<User[] | null | undefined>
const { data } = await api('/me')   // same inferred type

Override the response type per call:

const api = defineFetch({ baseURL: '/api' })

const { data: users }   = await api<User[]>('/users')    // Ref<User[] | null | undefined>
const { data: profile } = await api<UserProfile>('/me')  // Ref<UserProfile | null | undefined>

Transform — TypeScript tracks the output type through the second generic:

const api = defineFetch<User[]>({ baseURL: '/api' })

// <raw response, transform output>
const { data: names } = await api<User[], string[]>('/users', {
  transform: (users) => users.map(u => u.name),
})
// names: Ref<string[] | null | undefined>  ← transform output, not User[]

// TransformT is also inferred automatically when you annotate the input:
const { data: ids } = await api('/users', {
  transform: (users: User[]) => users.map(u => u.id),
})
// ids: Ref<number[] | null | undefined>

Call-site usage is identical to useFetch:

const { data, pending, error, refresh } = await useGetProfile({
  query: { include: 'posts' },
  lazy: true,
})

See Nuxt: useFetch options reference and ofetch interceptor docs.


defineAsyncData(defaults)

Creates a useAsyncData composable with pre-configured defaults. Useful when you control the fetch handler at the call site and want to co-locate the default value and SSR options with the resource type.

Generic type parameters:

| Parameter | Default | Description | |---|---|---| | DefaultDataT | unknown | Default response type for the factory | | DefaultTransformT | DefaultDataT | Default transform output type | | — | — | Call-site generics below | | ResT | DefaultDataT | Raw handler return type | | TransformT | ResT | Output type after applying transform |

// factory/listingsService.ts

// Set DefaultDataT so every composable using this factory is fully typed.
const asyncData = defineAsyncData<{ items: Listing[]; total: number }>({
  server: true,
  lazy: false,
  default: () => ({ items: [], total: 0 }),
})

// Each call to asyncData() is a distinct call site.
// The Vite transform injects a different _autoKey into each.
export const useListingsAll = () =>
  asyncData('all', () => $fetch('/api/listings'))

export const useListingsByCategory = (category: string) =>
  asyncData(category, () => $fetch('/api/listings', { query: { category } }))

autoKey: false — shared payload across all call-sites:

// All components calling useListingsAll() share one cached result.
const asyncData = defineAsyncData({ server: true, autoKey: false })

export const useListingsAll = () =>
  asyncData('listings-all', () => $fetch('/api/listings'))

Call-site usage mirrors useAsyncData(key, handler, options?). The _autoKey is prepended to the user-supplied key (when autoKey: true):

useListingsAll()              in PageA  →  "asyncData_<hash-A>:all"
useListingsByCategory('books')          →  "asyncData_<hash-B>:books"

Transform with typed output at the call site:

// TransformT inferred from the transform return type
const { data: titles } = await asyncData(
  'titles',
  () => $fetch<Listing[]>('/api/listings'),
  { transform: (res) => res.items.map(item => item.title) },
)
// data: Ref<string[] | null>  ← transform output

See Nuxt: useAsyncData reference.


withContext(composable)

Wraps a composable factory so that the Nuxt context survives any await inside it. Directly addresses the pattern discussed in nuxt/nuxt#33736.

// composables/useListings.ts
export const useListings = withContext(async (filters: ListingFilters = {}) => {
  // First fetch — context alive on first call, no wrapper needed here alone
  const { data: listings, pending } = await useApi('/listings', { query: filters })

  // Second fetch — would throw without withContext because the first await
  // cleared the Vue instance pointer. withContext ensures it still works.
  const { data: meta } = await useApi('/listings/meta')

  return { listings, meta, pending }
})
// pages/listings.vue
const { listings, meta, pending } = await useListings({ category: 'books' })

When to use withContext vs withContextAsync:

withContext works with both sync and async inner functions. withContextAsync is identical in behaviour but is explicitly typed as returning Promise — use it when you need async/await syntax at the wrapper level for try/catch blocks.


withContextAsync(composable)

Async-typed variant of withContext. The entire async function runs inside runWithContext, so every .then() continuation in the microtask chain is also scheduled within the restored context.

export const useUserProfile = withContextAsync(async (userId: string) => {
  const { data: user }    = await useFetch(`/api/users/${userId}`)
  const { data: posts }   = await useFetch(`/api/users/${userId}/posts`)
  const { data: follows } = await useFetch(`/api/users/${userId}/follows`)
  return { user, posts, follows }
})

Configuration

All options are optional. Defaults cover the most common setup.

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-fetch-factory'],

  fetchFactory: {
    // Default baseURL for all defineFetch factories that do not set their own.
    // Lowest-priority: factory-level and call-site baseURL always win.
    baseURL: '/api',

    // Scan src/factory/ and auto-import all its exports.
    // Change this if your service files live elsewhere.
    factoryDir: 'factory',

    // Inject unique _autoKey into every factory composable call-site.
    // Disable only if you manage keys entirely by hand.
    autoKey: true,

    // Auto-import defineFetch, defineAsyncData, withContext,
    // withContextAsync without explicit import statements.
    autoImport: true,

    // Register additional factory composable names for autoKey injection.
    // Needed only for factories that live outside factoryDir (e.g. composables/).
    factoryNames: [],
  },
})

| Option | Type | Default | Description | |---|---|---|---| | baseURL | string | '' | Default baseURL inherited by all defineFetch factories | | factoryDir | string | 'factory' | Directory relative to srcDir whose exports are auto-imported | | autoKey | boolean | true | Inject unique _autoKey per call-site at build time | | autoImport | boolean | true | Auto-import module utilities without explicit imports | | factoryNames | string[] | [] | Extra composable names to track for autoKey injection |


How auto-key injection works

The Vite transform plugin (enforce: 'post') runs two passes on every JS/TS/Vue file:

Phase 1 — Discovery: finds const X = defineFetch(...) and const X = defineAsyncData(...) declarations and registers X as a tracked name. The factoryDir is also pre-scanned in buildStart so that call-site files are keyed correctly even when Vite processes them before resolving the factory declaration files.

Phase 2 — Injection: for every call to a tracked name, computes SHA-1(composableName::relativeFilePath::callIndex) truncated to 8 hex characters and injects _autoKey: "<name>_<hash>" into the options object, or appends { _autoKey: "..." } as a trailing argument when no options object is present.

The injection is idempotent — a call that already contains _autoKey is left unchanged on subsequent builds. Source maps are preserved via magic-string.


Naming conventions

Exported composables from factory files should follow the standard use prefix convention. This keeps them consistent with Nuxt's built-in composables and prevents accidental shadowing in the auto-import registry.

// factory/userService.ts

// Private API client — not exported, no naming conflict possible
const api = defineFetch({ ... })

// Exported composables — use prefix avoids conflicts with Nuxt built-ins
export const useGetProfile   = () => api('/me')
export const useGetUserPosts = (id: string) => api(`/users/${id}/posts`)

Avoid exporting the raw factory instance directly — it looks like a plain function rather than a composable and bypasses the use prefix convention expected by Nuxt's auto-import scanner.

Using defineFetch outside the factory directory

If you prefer the composables/ convention, place your factory file directly there. Nuxt auto-imports its exports and the autoKey transform still applies:

// composables/useSearchService.ts
const api = defineFetch({ baseURL: '/api/search' })

export const useSearch = (query: string) => api('/', { query: { q: query } })
export const useSearchSuggestions = (query: string) =>
  api('/suggestions', { query: { q: query } })

// pages/index.vue — useSearch is auto-imported by Nuxt
const { data } = await useSearch('nuxt')

Development

This module uses @nuxt/module-builder for building and packaging.

# Install dependencies
npm install

# Build module stubs and generate playground types.
# Run this once after install, and again whenever src/ changes.
npm run dev:prepare

# Start the playground dev server
npm run dev

# Run tests
npm test

# Build the module for distribution (runs TypeScript compilation internally)
npm run build

How stub mode works (--stub): nuxt-module-build build --stub generates lightweight .mjs files in dist/ that re-export directly from the src/ sources. This means you can edit source files and see changes in the playground immediately without a full rebuild. The dist/ stubs are replaced by the real compiled output when you run npm run build.

Playground type checking: The playground's tsconfig.json extends .nuxt/tsconfig.json, which is generated by nuxi prepare as part of dev:prepare. This makes all Nuxt auto-imports, module augmentations, and generated types available to the IDE. Run npm run dev:prepare again after adding or renaming files in factory/ to refresh the auto-import declarations.

Module type checking: The root tsconfig.json covers only src/ and test/. Run npm run build to invoke the full TypeScript compiler pass that nuxt-module-build runs during packaging. This is the authoritative type check for the module's public API.


Background and prior art

This module implements the factory composable pattern proposed by @pi0 in nuxt/nuxt#14736 and applies the runWithContext fix discussed in nuxt/nuxt#33736.

The Nuxt documentation covers a simpler version of the custom useFetch pattern at nuxt.com/docs/guide/recipes/custom-usefetch. This module extends that pattern with automatic key generation, interceptor composition, and context restoration utilities.


License

MIT