@ametie/vue-muza-use
@ametie/vue-muza-use 🎹
TypeScript-first, feature-rich Axios wrapper for Vue 3 Composition API. Built for real-world business logic.
A production-ready composable that eliminates boilerplate and solves the hard problems: race conditions, token refresh queues, automatic retries, and reactive request management. Write less code, ship faster, sleep better.
[!IMPORTANT]
🤖 Claude Code — Built-in AI Skill
This library ships with a skill that teaches Claude the feature wrapper pattern, naming conventions, and all
UseApiOptions. Claude will generate correct, architecture-consistent API layer code out of the box — no extra prompting needed.
Using v0.x? The legacy documentation is available at v0.10.0 README.
✨ Features
Core Features (Get started in minutes):
- 🎯 TypeScript-first — Full TypeScript support with strict typing for requests and responses
- 🔄 Smart Reactivity — Auto-tracks reactive deps in
url,params, anddata— refetches automatically when they change - ⏱️ Built-in Debouncing — Perfect for search inputs and auto-save forms
- 🛡️ Race Condition Protection — Global abort controller cancels stale requests automatically
- 📊 Auto-Polling — Built-in interval fetching with smart tab visibility detection
- 🚀 Batch Requests — Execute multiple requests in parallel with progress tracking
- 🧹 Zero Memory Leaks — Automatic cleanup of pending requests on component unmount
- 🔕 ignoreUpdates — Update reactive deps silently without triggering a re-fetch
- 🗄️ Response Caching — In-memory cache with configurable TTL and manual invalidation
- ⚡ Stale-While-Revalidate — Serve cached data instantly while refreshing silently in the background
- 🔬 select — Transform or filter response data declaratively; re-applied on every fetch automatically
Advanced Features (When you need them):
- ♻️ Intelligent Retries — Lifecycle-aware retry logic with configurable status codes
- 🔐 JWT Token Management — Automatic token refresh with request queueing on 401 responses
- 🎛️ Flexible Architecture — Bring your own Axios instance with full configuration control
- 🍪 withCredentials — Per-request cookie and cross-origin credential control
- 🔭 DevTools Panel — Inspect live requests, payloads, and instance state via
@ametie/vue-muza-devtools
🆚 How it compares
Honest comparison. ✅ built-in · ⚠️ partial or plugin needed · ❌ not supported
| Feature | vue-muza-use | @vueuse/useFetch | TanStack Query | swrv |
|---|---|---|---|---|
| Axios-first | ✅ | ❌ fetch | ⚠️ adapter | ❌ fetch |
| JWT auto-refresh + queue | ✅ | ❌ | ❌ | ❌ |
| Race condition protection | ✅ | ❌ | ✅ | ❌ |
| ignoreUpdates | ✅ | ❌ | ❌ | ❌ |
| Built-in debounce | ✅ | ❌ | ❌ | ❌ |
| Batch requests | ✅ | ❌ | ❌ | ❌ |
| Built-in retry | ✅ | ❌ | ✅ | ❌ |
| Auto-polling | ✅ | ❌ | ✅ | ✅ |
| SWR (stale-while-revalidate) | ✅ | ❌ | ✅ | ✅ |
| select / transform | ✅ | ❌ | ✅ | ❌ |
| Response caching | ✅ | ❌ | ✅ | ✅ |
| TypeScript | ✅ | ✅ | ✅ | ✅ |
| SSR / Nuxt | ❌ | ✅ | ✅ | ✅ |
| DevTools | ✅ | ❌ | ✅ | ❌ |
Choose vue-muza-use if: you build Vue 3 SPAs with Axios, need JWT token refresh out of the box, and want reactive request management without a heavyweight server-state solution.
Choose TanStack Query if: you need SSR or advanced server-state normalization.
Choose @vueuse/useFetch if: you want a minimal fetch wrapper with no opinions.
📖 Table of Contents
Getting Started:
Core Features:
- Method Helpers (useApiGet, useApiPost …)
- Watch & Auto-Refetch
- Response Caching
- Refetch Triggers
- Polling (Background Updates)
- Error Handling
- Loading States
- Manual Data Updates (mutate)
- select — Declarative Data Transformation
- execute() — Per-Call Option Overrides
Real-World Examples:
Advanced:
- Advanced Configuration
- DevTools Panel
- Authentication & Token Management
- Error Handling Reference
- Utilities & Standalone Composables
- API Reference
- Common Patterns
- Gotchas
- Troubleshooting
💡 New to the library? Start with Quick Start, then explore Basic Usage. Skip authentication until you need it!
📦 Installation
# npm
npm install @ametie/vue-muza-use axios
# pnpm
pnpm add @ametie/vue-muza-use axios
# yarn
yarn add @ametie/vue-muza-use axios
Peer dependencies are packages you need to install separately — the library uses them but doesn't bundle them. You need vue (≥ 3.x) and axios (≥ 1.x) in your project.
🚀 Quick Start
Get started in 2 minutes with minimal configuration.
1. Setup Plugin (main.ts)
import { createApp } from 'vue'
import { createApi, createApiClient } from '@ametie/vue-muza-use'
import App from './App.vue'
const app = createApp(App)
const api = createApiClient({
baseURL: 'https://api.example.com'
})
app.use(createApi({ axios: api }))
app.mount('#app')
💡 That's it! No auth configuration needed to get started. Add it later when you need it.
2. Your First Request
<script setup lang="ts">
import { useApi } from '@ametie/vue-muza-use'
interface User {
id: number
name: string
email: string
}
const { data, loading, error } = useApi<User>('/users/1', {
immediate: true
})
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else-if="data">
<h1>{{ data.name }}</h1>
<p>{{ data.email }}</p>
</div>
</template>
3. Live Search with Debounce
This example shows the library's power: automatic race condition handling and debouncing built-in.
<script setup lang="ts">
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
interface Product {
id: number
name: string
price: number
}
const searchQuery = ref('')
const { data, loading } = useApi<Product[]>(
() => `/products/search?q=${searchQuery.value}`,
{
debounce: 500
}
)
</script>
<template>
<input v-model="searchQuery" placeholder="Search products..." />
<div v-if="loading">Searching...</div>
<ul v-else-if="data?.length">
<li v-for="product in data" :key="product.id">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
<p v-else-if="searchQuery">No results found</p>
</template>
4. POST with Retry
Use retry to automatically re-attempt failed form submissions before showing an error.
<script setup lang="ts">
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
interface CreateOrderResponse {
id: number
status: string
}
const form = ref({ productId: 1, quantity: 2 })
const { execute, loading, error } = useApi<CreateOrderResponse>(
'/orders',
{
method: 'POST',
data: form,
retry: 3,
retryDelay: 1000,
onSuccess: (response) => {
console.log('Order created:', response.data.id)
}
}
)
</script>
<template>
<button :disabled="loading" @click="execute()">
{{ loading ? 'Placing order...' : 'Place Order' }}
</button>
<p v-if="error">{{ error.message }}</p>
</template>
📖 Basic Usage
GET Requests
Manual Execution
import { useApi } from '@ametie/vue-muza-use'
interface User {
id: number
name: string
}
const { data, loading, error, execute } = useApi<User>('/users/1')
await execute()
Auto-Fetch on Mount
import { useApi } from '@ametie/vue-muza-use'
interface User {
id: number
name: string
}
const { data } = useApi<User>('/users/1', {
immediate: true
})
With Query Parameters
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
const filters = ref({
status: 'active',
limit: 20
})
const { data } = useApi('/users', {
params: filters,
immediate: true
})
Conditional Fetching
Pass a getter function that returns undefined to prevent a request from firing until a required value is available.
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
interface User {
id: number
name: string
}
const id = ref<number | null>(null)
const { data } = useApi<User>(
() => id.value ? `/users/${id.value}` : undefined,
)
// No request fires until id.value is set
id.value = 42 // → triggers request to /users/42
[!NOTE] When the URL getter returns
undefined, the request throws internally with "Request URL is missing". This error is surfaced inerror.valuelike any other request failure, so your error handling works as expected.
POST/PUT/PATCH Requests
Simple Form Submission
<script setup lang="ts">
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
interface LoginResponse {
accessToken: string
refreshToken: string
}
const form = ref({
email: '',
password: ''
})
const { execute, loading, error } = useApi<LoginResponse>(
'/auth/login',
{
method: 'POST',
data: form,
authMode: 'public',
onSuccess: (response) => {
console.log('Logged in!', response.data.accessToken)
}
}
)
</script>
<template>
<form @submit.prevent="execute()">
<input v-model="form.email" type="email" />
<input v-model="form.password" type="password" />
<button :disabled="loading">
{{ loading ? 'Signing in...' : 'Sign In' }}
</button>
<p v-if="error">{{ error.message }}</p>
</form>
</template>
🎯 Core Features
Method Helpers (useApiGet, useApiPost …)
Thin wrappers over useApi that pre-set the HTTP method. Use them to skip the method option on every call.
import {
useApiGet,
useApiPost,
useApiPut,
useApiPatch,
useApiDelete,
} from '@ametie/vue-muza-use'
// Equivalent to useApi('/users', { immediate: true })
const { data } = useApiGet<User[]>('/users', { immediate: true })
// Equivalent to useApi('/users', { method: 'POST', data: form })
const { execute, loading } = useApiPost<User>('/users', { data: form })
// PUT / PATCH / DELETE follow the same pattern
const { execute: update } = useApiPut<User>(`/users/${id}`, { data: form })
const { execute: patch } = useApiPatch<User>(`/users/${id}`, { data: partial })
const { execute: remove } = useApiDelete(`/users/${id}`)
All helpers accept the same UseApiOptions as useApi — every feature (cache, retry, polling, callbacks) works identically.
💡 These are the preferred way to write feature API wrappers. See API layer pattern in the skill file, or the feature composable examples below.
Watch & Auto-Refetch
TL;DR: Pass reactive refs or getters to url, params, or data — the request re-fires automatically when they change.
Any reactive dependency accessed inside a getter is tracked automatically. No explicit watch option needed.
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
const search = ref('')
const page = ref(1)
// Reactive params getter — both search and page are tracked automatically
const { data, loading } = useApi('/products', {
params: () => ({ q: search.value, page: page.value }),
immediate: true,
})
// Change any dep → request re-fires automatically
search.value = 'shoes'
page.value = 2
Reactive URL:
const userId = ref(1)
const { data } = useApi(() => `/users/${userId.value}`, {
immediate: true,
})
userId.value = 2 // → re-fetches /users/2 automatically
Opt-out with lazy: true:
For forms and manual mutations where you control when execute() is called:
const form = ref({ name: '', email: '' })
const { execute, loading } = useApi('/users', {
method: 'POST',
data: form,
lazy: true, // form changes do NOT trigger re-fetch
onSuccess: () => router.push('/users'),
})
// Only fires when you call it
async function submit() {
await execute()
}
immediate works independently of auto-tracking:
// Fetch on mount + re-fetch on dep change
useApi('/products', {
params: () => ({ q: search.value }),
immediate: true,
})
// No fetch on mount, but re-fetch on dep change
useApi('/products', {
params: () => ({ q: search.value }),
// immediate: false is the default
})
Batching: Vue batches synchronous reactive changes before triggering auto-tracking — two synchronous ref changes fire only one request.
// Only one request fires (Vue batches sync changes)
status.value = 'active'
page.value = 1
ignoreUpdates — Update Without Re-fetching
TL;DR: Update a reactive dep without triggering auto-tracking.
When auto-tracking is active, any reactive dep change fires a new request. ignoreUpdates pauses the tracking scope for the duration of the callback — changes inside do not trigger a re-fetch.
Example — clear search input without fetching
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
const search = ref('')
const { data, ignoreUpdates } = useApi('/products', {
params: () => ({ q: search.value }),
debounce: 300,
})
function clearSearch() {
ignoreUpdates(() => {
search.value = ''
})
// auto-tracking is paused — no request fires
}
The user types → auto-tracking fires → debounced request. Clicking "Clear" resets the input without triggering a fetch.
Safe to call when lazy: true
If lazy: true, ignoreUpdates still runs the updater — it just has nothing to suppress.
const { ignoreUpdates } = useApi('/data')
ignoreUpdates(() => {
someRef.value = 42 // runs normally, nothing to suppress
})
[!NOTE]
ignoreUpdatesis synchronous only. Changes made after anawaitinside the updater function will NOT be suppressed — the flag resets after the synchronous portion completes.
Response Caching
TL;DR: Pass cache: 'key' to serve repeated requests from memory instead of the network. Entries expire after 5 minutes by default.
The cache is an in-memory Map shared across all useApi instances in the app.
It is intentionally simple: no reactive subscriptions, no persistence, no background timers.
Entries expire lazily — stale entries are removed the next time they are read.
Basic Usage — String Shorthand
<script setup lang="ts">
import { useApi } from '@ametie/vue-muza-use'
const { data, loading } = useApi<Category[]>('/categories', {
cache: 'categories', // uses DEFAULT_STALE_TIME (5 minutes)
immediate: true,
})
</script>
The first call hits the network and caches the result under the key 'categories'.
Every subsequent execute() within 5 minutes is served from cache instantly — loading never becomes true and no axios request is made.
Automatic Cache Keys — cache: true
TL;DR: Omit the id and the key is derived from method + url + params + data, so every page / filter combination gets its own cache entry automatically.
A static string id is a footgun for paginated or filtered lists: the same key is reused for every page, so a cache hit serves the wrong page. Pass cache: true (or an object without id) and the key is built at request time from the actual request:
<script setup lang="ts">
const page = ref(1)
const filters = ref({ brand: ['ART'], channel: [10, 1] })
const { data, cacheKey } = useApi<Product[]>('/products', {
// key = auto:GET:/products:{params}:{data} — unique per page + filter set
cache: true,
params: () => ({ page: page.value }),
data: () => filters.value,
immediate: true,
})
// Flipping `page` fetches each page once; returning to a visited page is instant.
</script>
- Params and body are serialized with sorted keys, so
{ a, b }and{ b, a }map to the same entry; array order is significant ([10, 1] ≠ [1, 10]). cache: true≡cache: {}. Add fields to override defaults while keeping the auto key:cache: { swr: true, freshFor: '1m' }.- Provide an explicit
idto opt out and pin a manual key:cache: { id: 'products' }. - The resolved key is exposed as
cacheKey(aRef<string | null>) on the return — pass it toinvalidateCache(cacheKey.value)to bust that exact entry, or use prefix invalidation (below) to bust every page at once.
Custom TTL — CacheOptions Object
<script setup lang="ts">
import { useApi } from '@ametie/vue-muza-use'
const { data, execute } = useApi<Product[]>('/products', {
cache: {
id: 'products',
staleTime: 60_000, // 1 minute
},
immediate: true,
})
</script>
Cache Hit Behavior
When a valid cache entry is found:
| Property / Hook | Cache Hit |
|---|---|
loading |
stays false — never set to true |
data |
updated immediately via mutate() |
onBefore |
not called |
onSuccess |
not called |
onFinish |
not called |
| axios request | not made |
This is intentional — a cache hit is silent. If you need to know when data comes from cache vs the network, track it with onSuccess (only fires on network hits).
invalidateCache — Bust Related Caches on Mutation
Use invalidateCache on a POST/PUT/DELETE to automatically clear caches when the mutation succeeds.
<script setup lang="ts">
import { useApi } from '@ametie/vue-muza-use'
// GET — caches the list
const { data: products, execute: reload } = useApi<Product[]>('/products', {
cache: 'products',
immediate: true,
})
// POST — busts the list cache on success so the next GET hits the network
const { execute: createProduct, loading } = useApi('/products', {
method: 'POST',
invalidateCache: 'products',
})
async function submit(form: NewProduct) {
await createProduct({ data: form })
await reload() // cache is gone — fetches fresh data
}
</script>
invalidateCache fires only on HTTP 2xx success. It never runs in catch or finally.
Pass an array to bust multiple keys at once:
const { execute } = useApi('/orders', {
method: 'POST',
invalidateCache: ['orders', 'products', 'inventory'],
})
Imperative Cache Control
Import invalidateCache or clearAllCache anywhere in your app — outside components, in Pinia stores, in route guards:
import { invalidateCache, clearAllCache } from '@ametie/vue-muza-use'
// Bust a single key (e.g. after a WebSocket push)
invalidateCache('products')
// Bust multiple keys at once
invalidateCache(['products', 'categories'])
// Bust every auto-keyed variation of an endpoint by key prefix —
// e.g. all cached pages/filters of the products list after a create/update
invalidateCache({ prefix: 'auto:GET:/products' })
// Several prefixes in one pass — one sweep over the cache instead of three calls
invalidateCache({ prefix: ['auto:GET:/products', 'auto:GET:/categories'] })
// Wipe everything — call on logout to prevent data leaks between users
clearAllCache()
[!NOTE] Empty prefixes are ignored —
{ prefix: '' },{ prefix: [] }and{ prefix: ['', ''] }are all no-ops and will not wipe the whole cache. UseclearAllCache()for that.
cache + auto-tracking
When auto-tracking is active, each dep-change-triggered execute() still checks the cache first:
<script setup lang="ts">
import { useApi } from '@ametie/vue-muza-use'
import { ref } from 'vue'
const categoryId = ref<number>(1)
const { data } = useApi<Product[]>(() => `/categories/${categoryId.value}/products`, {
cache: { id: `products-cat-${categoryId.value}`, staleTime: 30_000 },
params: () => ({ category: categoryId.value }),
immediate: true,
})
</script>
[!NOTE] The cache
idis evaluated once whenuseApiis called. To cache per category, use a computed or a dynamic key string derived from your reactive state.
cache + retry
Cache is written after the final successful attempt, not after the first. If the first attempt fails and a retry succeeds, the retry's response is cached:
const { data } = useApi('/reports', {
cache: 'reports',
retry: 2,
retryStatusCodes: [500, 503],
immediate: true,
})
Complete Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
cache |
string | boolean | CacheOptions |
undefined |
Enable caching. true/{} = auto-key + defaults; string = manual key shorthand |
invalidateCache |
string | string[] | { prefix: string | string[] } |
undefined |
Cache key(s) or a key prefix to delete on 2xx success |
CacheOptions
| Field | Type | Default | Description |
|---|---|---|---|
id |
string |
(auto) | Manual cache key. Omit to auto-derive the key from method + url + params + data |
staleTime |
DurationInput |
300_000 |
TTL — milliseconds or a duration string ('5m', '1h'). Entry is deleted on next read after this time |
swr |
boolean |
false |
Stale-while-revalidate: serve cached data instantly while revalidating in the background. See SWR |
freshFor |
DurationInput |
0 |
Age below which an SWR hit is served without background revalidation. Only meaningful with swr: true. See freshFor |
DurationInput — every duration field accepts either milliseconds (number) or a
human-readable string: '500ms', '30s', '5m', '1.5h', '1d'. Typos in the unit
('5x') are TypeScript errors, and '1h' can't silently mean the wrong number of zeros:
cache: { id: 'report', staleTime: '1d' } // ✅ 86_400_000 ms — no arithmetic, no mistakes
cache: { id: 'report', staleTime: 24_000_000 } // ⚠️ compiles, but this is ~6.7h, not 24h
Out of Scope (by design)
The following are intentionally not supported in v1:
- 🚫 No reactive cache entries — the cache is a plain
Map, not Vue refs - 🚫 No
localStorage/sessionStoragepersistence - 🚫 No background TTL timers — expiry is checked lazily on read
- 🚫 No manual cache
id/swrforuseApiBatch— batch caching is auto-keyed per request (seeuseApiBatchoptions) - 🚫 No automatic refetch on cache invalidation — call
execute()manually after invalidating - 🚫 No request deduplication — concurrent calls for the same key each fire their own request
[!WARNING] The cache store is module-level (a singleton). In SSR / Node.js environments it is shared between all incoming requests. Call
clearAllCache()between requests or avoid using caching in SSR contexts.
Stale-While-Revalidate (SWR)
TL;DR: Return cached data instantly while fetching fresh data in the background. No loading spinner, no blank screen.
Requires the cache option to be set. On a cache hit, the stale data is returned immediately (no loading: true, no spinner) while a silent background request runs. Use the revalidating ref to show a subtle refresh indicator if needed.
On a cache miss (first load), the request behaves exactly like a normal request — loading: true, no stale data.
Basic Usage
<script setup lang="ts">
import { useApi } from '@ametie/vue-muza-use'
interface User { id: number; name: string }
const { data, revalidating } = useApi<User[]>('/users', {
cache: { id: 'users', swr: true },
immediate: true,
})
</script>
<template>
<!-- data renders immediately from cache — no blank screen -->
<ul>
<li v-for="user in data" :key="user.id">
{{ user.name }}
<span v-if="revalidating">↻</span>
</li>
</ul>
</template>
SWR vs Normal Cache Hit
| Normal cache hit | SWR cache hit | |
|---|---|---|
loading |
false |
false |
data |
Stale data, no new request | Stale data → then fresh data |
revalidating |
false |
true while fetching, then false |
| Axios request | Not made | Made (silent background fetch) |
onBefore |
Not called | Not called (silent) |
onSuccess |
Not called | Called with fresh response |
onFinish |
Not called | Called after background fetch |
Error Handling
If the background revalidation request fails:
revalidatingresets tofalseerroris set- The stale data is preserved — your UI doesn't go blank
const { data, revalidating, error } = useApi('/dashboard', {
cache: { id: 'dashboard', swr: true },
immediate: true,
})
// data.value stays the cached value even after a failed revalidation
freshFor — Skip Revalidation While Data Is Fresh
TL;DR: freshFor adds a "fresh enough, don't even revalidate" tier — SWR stops hitting the network on every single cache hit.
By default (freshFor: 0) every SWR cache hit fires a background request. With freshFor
set, the entry's age decides what happens:
| Entry age | Behavior |
|---|---|
under freshFor |
cache served, no network, revalidating stays false |
freshFor → staleTime |
cache served instantly + silent background revalidation |
past staleTime |
entry deleted — normal request with loading: true |
// Daily report: instant display, network at most once an hour,
// loading spinner only when data is more than a day old.
const { data, revalidating } = useApi('/daily-report', {
cache: { id: 'report', swr: true, freshFor: '1h', staleTime: '1d' },
immediate: true,
})
Using SWR as a drop-in upgrade for the plain cache. Take your current staleTime,
move it to freshFor, and give staleTime a larger value:
// Before: within 1m — silent cache; after 1m — full request with a loading flash
cache: { id: 'products', staleTime: '1m' }
// After: within 1m — identical silent cache; between 1m and 1h — instant data
// with an invisible background refresh; loading flash only past 1h
cache: { id: 'products', swr: true, freshFor: '1m', staleTime: '1h' }
Freshness is evaluated per read, per consumer — two useApi instances sharing one
cache id can use different freshFor values (e.g. a dashboard widget tolerates '1h',
a detail page wants '1m').
An explicit invalidateCache(id) always wins: the entry is gone, the next call is a
normal network request regardless of freshFor.
Refetch Triggers
TL;DR: Automatically re-fetch when the user returns to the tab or regains connectivity — no manual wiring needed.
refetchOnFocus — Re-fetch on Tab Return
const { data } = useApi('/dashboard', {
immediate: true,
refetchOnFocus: true, // default throttle: 60s
})
Pass { throttle: 0 } to always re-fetch regardless of how recently data was loaded:
const { data } = useApi('/notifications', {
immediate: true,
refetchOnFocus: { throttle: 0 },
})
Works seamlessly with cache: { swr: true } — the user sees stale data instantly, fresh data arrives silently in the background:
const { data, revalidating } = useApi('/feed', {
cache: { id: 'feed', swr: true },
refetchOnFocus: true,
immediate: true,
})
refetchOnReconnect — Re-fetch on Network Restore
const { data } = useApi('/messages', {
immediate: true,
refetchOnReconnect: true,
})
Global Configuration
Apply to all useApi instances at once:
const api = createApiClient({ baseURL: 'https://api.example.com' })
app.use(createApi({
axios: api,
globalOptions: {
refetchOnFocus: true,
refetchOnReconnect: true,
// Project-wide cache behavior — applied per-field under each request's own `cache`.
cacheDefaults: { swr: true, staleTime: '6h', freshFor: '30m' },
},
}))
With cacheDefaults above, cache: true on any request inherits SWR + those timings, so you write it once instead of on every composable:
// Inherits swr / staleTime / freshFor from cacheDefaults; auto-keyed.
const { data } = useApi('/products', { cache: true, immediate: true })
// Override a single field; the rest still come from cacheDefaults.
const { data: report } = useApi('/report', { cache: { freshFor: '5m' }, immediate: true })
[!IMPORTANT]
cacheDefaultsnever enables caching by itself — a request is cached only if it explicitly passescache. This prevents accidentally caching mutations. AnyidinsidecacheDefaultsis ignored.
Merge precedence (per-field, later wins): cacheDefaults < composable cache < per-call execute({ cache }).
Opt individual requests out with refetchOnFocus: false:
// Global refetchOnFocus: true, but this request opts out
const { data } = useApi('/static-config', {
refetchOnFocus: false,
immediate: true,
})
Polling (Background Updates)
TL;DR: Keep data fresh with smart polling that automatically pauses when the browser tab is hidden.
Simple Polling
import { useApi } from '@ametie/vue-muza-use'
const { data } = useApi('/notifications', {
immediate: true,
poll: 5000
})
Dynamic Polling Control
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
const pollInterval = ref(3000)
const { data } = useApi('/live-feed', {
poll: pollInterval,
immediate: true
})
pollInterval.value = 0 // Stop polling
pollInterval.value = 5000 // Resume with new interval
Polling Object Syntax with Reactive Fields
Both interval and whenHidden can be reactive refs — change them at runtime without re-creating the composable.
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
const interval = ref(5000)
const whenHidden = ref(false)
const { data } = useApi('/status', {
immediate: true,
poll: { interval, whenHidden }
})
// Slow down polling
interval.value = 30000
// Allow polling even when tab is not visible
whenHidden.value = true
// Stop polling completely
interval.value = 0
[!NOTE] By default
whenHidden: false— polling pauses when the browser tab is hidden and resumes automatically when the user switches back. SetwhenHidden: truefor background jobs that must continue regardless of tab visibility.
Error Handling
Per-Request Error Handling
import { useApi } from '@ametie/vue-muza-use'
const { error, execute } = useApi('/users', {
onError: (error) => {
if (error.status === 404) {
console.error('User not found')
} else {
console.error('Something went wrong')
}
},
skipErrorNotification: true
})
retry — Automatic Request Retry
TL;DR: Automatically retry failed requests before showing an error.
Retries fire only after the request fails. The loading state stays true during all attempts. onError is only called after the final failure.
retry option
| Value | Meaning |
|---|---|
false |
Never retry (default) |
true |
Retry up to 3 times |
3 |
Retry exactly 3 times |
retryDelay
How many milliseconds to wait between retry attempts. Default: 1000 (1 second).
retryStatusCodes — The Priority Chain
retryStatusCodes controls which HTTP status codes should trigger a retry. The library uses a three-level priority chain:
Per-request retryStatusCodes
↓ (if not set)
globalOptions.retryStatusCodes
↓ (if not set)
Library default: [408, 429, 500, 502, 503, 504]
- Per-request:
retryStatusCodesinuseApi()options — highest priority, overrides everything - globalOptions:
retryStatusCodesincreateApi()— applies to all requests that don't set their own - Library default:
[408, 429, 500, 502, 503, 504]— used when neither level is configured
[!NOTE]
retryStatusCodes: []means retry on ANY error — network errors, timeouts, and any non-2xx response. This is an explicit opt-in, not the default.
[!WARNING] Retry does NOT fire on
AbortError(cancelled requests) or when the component unmounts during a retry delay — the library cleans up safely in both cases.
Examples
Retry only on server errors (500, 503):
import { useApi } from '@ametie/vue-muza-use'
const { data } = useApi('/reports', {
immediate: true,
retry: 3,
retryDelay: 2000,
retryStatusCodes: [500, 503]
})
Retry on any error including network failures:
import { useApi } from '@ametie/vue-muza-use'
const { data } = useApi('/critical-data', {
immediate: true,
retry: 5,
retryStatusCodes: [] // retry on any error
})
Global default with per-request override:
import { createApp } from 'vue'
import { createApi, createApiClient, useApi } from '@ametie/vue-muza-use'
// main.ts — global: retry 2 times on server errors
const api = createApiClient({ baseURL: 'https://api.example.com' })
createApp(App).use(createApi({
axios: api,
globalOptions: {
retry: 2,
retryStatusCodes: [500, 502, 503, 504]
}
}))
// In a component — override: retry only once for this request
const { data } = useApi('/payments', {
immediate: true,
retry: 1,
retryStatusCodes: [500]
})
Loading States
Per-Request Loading
import { useApi } from '@ametie/vue-muza-use'
const { data: user, loading: userLoading } = useApi('/user')
const { data: posts, loading: postsLoading } = useApi('/posts')
Lifecycle Hooks
import { useApi } from '@ametie/vue-muza-use'
const { execute } = useApi('/analytics', {
onBefore: () => {
console.log('Request starting...')
},
onSuccess: (response) => {
console.log('Success!', response.data)
},
onError: (error) => {
console.error('Failed:', error.message)
},
onFinish: () => {
console.log('Request finished (success or error)')
}
})
Manual Data Updates (mutate)
TL;DR: Update local data optimistically or post-process fetched data without re-fetching.
Use mutate to manually update the data ref. Supports direct values or updater functions (like React's setState). Calling mutate automatically clears any existing error.
Add/Remove/Update Items
import { useApi } from '@ametie/vue-muza-use'
interface Todo {
id: number
title: string
done: boolean
}
const { data, mutate } = useApi<Todo[]>('/todos', { immediate: true })
const addTodo = (newTodo: Todo) => {
mutate(prev => prev ? [...prev, newTodo] : [newTodo])
}
const removeTodo = (id: number) => {
mutate(prev => prev?.filter(t => t.id !== id) ?? null)
}
const toggleTodo = (id: number) => {
mutate(prev =>
prev?.map(t => t.id === id ? { ...t, done: !t.done } : t) ?? null
)
}
Transform after fetch
Use mutate from the same useApi call to post-process data after it arrives:
import { useApi } from '@ametie/vue-muza-use'
interface User {
id: number
firstName: string
lastName: string
fullName?: string
}
const { data, mutate } = useApi<User[]>('/users', {
immediate: true,
onSuccess: ({ data: users }) => {
mutate(users.map(u => ({
...u,
fullName: `${u.firstName} ${u.lastName}`
})))
}
})
[!TIP] If the same transformation runs on every fetch (including polling or watch re-triggers), use
selectinstead — it's applied automatically and keeps your options object clean.
select — Declarative Data Transformation
TL;DR: Transform, filter, or reshape response data once — it's re-applied automatically on every fetch, polling tick, and SWR revalidation.
Use select when you want the same transformation applied every time the request fires. Unlike mutate (which you call manually), select is declared once and runs silently on each response.
The second generic parameter of useApi controls the output type of select.
Extract a Nested Field
APIs that wrap responses in { data: [...], meta: {...} }:
interface ApiResponse { data: User[]; meta: { total: number } }
interface User { id: number; name: string }
const { data } = useApi<ApiResponse, User[]>('/users', {
immediate: true,
select: (res) => res.data,
// data.value is User[], not ApiResponse
})
Transform Items
interface RawUser { id: number; firstName: string; lastName: string }
interface User { id: number; fullName: string }
const { data } = useApi<RawUser[], User[]>('/users', {
immediate: true,
select: (users) => users.map(u => ({
id: u.id,
fullName: `${u.firstName} ${u.lastName}`,
})),
})
Filter Results
const { data } = useApi<Task[]>('/tasks', {
immediate: true,
select: (tasks) => tasks.filter(t => t.status === 'active'),
})
select vs mutate
select |
mutate |
|
|---|---|---|
| When it runs | On every successful response (auto) | When you call it manually |
| With polling | Re-applied on every tick | Need to call in onSuccess each time |
| With SWR | Re-applied on revalidation | Need to call in onSuccess |
onSuccess receives |
Raw AxiosResponse<TRaw> |
— |
[!NOTE]
onSuccessalways receives the rawAxiosResponsefrom the server, not the selected value. This lets you access headers, status, and the original shape if needed.
[!NOTE] The cache always stores the raw server response, not the selected value.
selectis re-applied each time data is read from cache — including SWR cache hits. If you change yourselectfunction, the next cache hit will re-apply the new transformation.
execute() — Per-Call Option Overrides
execute() accepts an optional ExecuteConfig that overrides composable-level options for that specific call only. The next call reverts to the composable defaults.
Lifecycle callbacks merge — both composable-level and per-call handlers fire (composable first).
All other options replace — the per-call value wins for that request.
import { useApiPost } from '@ametie/vue-muza-use'
const { execute, loading } = useApiPost<User>('/users', {
onSuccess: () => refreshList(), // fires on every execute()
invalidateCache: 'users-count', // invalidated on every execute()
})
// This call also shows a toast — both onSuccess handlers fire
await execute({
data: { name: 'Alice' },
onSuccess: () => toast('User created!'),
})
// This call silences the global error handler and uses a different cache key
await execute({
data: { name: 'Bob' },
skipErrorNotification: true,
invalidateCache: ['users-count', 'users-list'],
})
Options you can override per call:
| Category | Options |
|---|---|
| Request | data, params, headers, method, authMode, withCredentials, signal |
| Caching | cache (replaces), invalidateCache (replaces) |
| Retry | retry, retryDelay, retryStatusCodes |
| Error | skipErrorNotification |
| Lifecycle | onBefore, onSuccess, onError, onFinish (all merge) |
Options that cannot be overridden per call (setup-time only): immediate, lazy, debounce, poll, refetchOnFocus, refetchOnReconnect, initialData, initialLoading, useGlobalAbort.
📊 Real-World Examples
Data Table with Pagination & Sorting
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
interface Order {
id: number
created_at: string
total: number
}
interface OrdersResponse {
data: Order[]
total: number
}
const page = ref(1)
const sortBy = ref('created_at')
const sortOrder = ref<'asc' | 'desc'>('desc')
const params = computed(() => ({
page: page.value,
sort_by: sortBy.value,
sort_order: sortOrder.value,
per_page: 20
}))
const { data, loading } = useApi<OrdersResponse>('/orders', {
params,
immediate: true
})
</script>
<template>
<table>
<thead>
<tr>
<th @click="sortBy = 'id'">ID</th>
<th @click="sortBy = 'created_at'">Date</th>
<th @click="sortBy = 'total'">Total</th>
</tr>
</thead>
<tbody v-if="!loading">
<tr v-for="order in data?.data" :key="order.id">
<td>{{ order.id }}</td>
<td>{{ order.created_at }}</td>
<td>\${{ order.total }}</td>
</tr>
</tbody>
</table>
</template>
Request Cancellation
import { useAbortController, useApi } from '@ametie/vue-muza-use'
const { abortAll } = useAbortController()
const { data: products } = useApi('/products')
const { data: stats } = useApi('/stats')
const resetFilters = () => {
abortAll()
}
Batch Requests
TL;DR: Execute multiple API requests in parallel with full reactive state, progress tracking, and error tolerance.
Basic Usage
import { useApiBatch } from '@ametie/vue-muza-use'
interface User {
id: number
name: string
}
const {
successfulData,
loading,
progress,
execute
} = useApiBatch<User>([
'/users/1',
'/users/2',
'/users/3'
])
await execute()
console.log(successfulData.value)
console.log(progress.value)
Per-Request Config (BatchRequestConfig)
TL;DR: Each request in the batch can have its own method, body, and headers.
Pass objects instead of strings to specify per-request configuration. You can mix strings (simple GET) and config objects in the same array.
import { useApiBatch } from '@ametie/vue-muza-use'
interface User { id: number; name: string }
interface Post { id: number; title: string }
const { data, execute } = useApiBatch([
'/users/1',
{
url: '/users',
method: 'POST',
data: { name: 'Alice', email: 'alice@example.com' }
},
{
url: '/posts',
method: 'GET',
params: { userId: 1 }
},
{
url: '/analytics/track',
method: 'POST',
headers: { 'X-Source': 'dashboard' },
data: { event: 'page_view' }
}
])
await execute()
BatchRequestConfig interface:
interface BatchRequestConfig {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' // default: 'GET'
data?: unknown
params?: unknown
headers?: Record<string, string>
client?: string // named client for this item — overrides the batch-level `client` (1.9+)
}
BatchResultItem — What Each Result Contains
Every item returned in data has this shape:
| Field | Type | Description |
|---|---|---|
url |
string |
The URL that was requested |
index |
number |
Position in the original array |
success |
boolean |
true if the request succeeded — equivalent to status === "success" |
status |
"pending" | "success" | "error" | "aborted" |
Lifecycle of this item. pending means no result yet; aborted means the request was cancelled before it answered (see Cancelling a batch) |
stale |
boolean |
true while data comes from cache and a background refresh is in flight or has failed |
data |
T | null |
Response data. null while pending, and null on a failure with no cached value to fall back on |
error |
ApiError | null |
Error details (null if succeeded) |
statusCode |
number | null |
HTTP status code |
response |
AxiosResponse<T> | null |
Full Axios response — access headers here (null if failed) |
request |
BatchRequestConfig |
The original normalized request config |
Accessing response headers from a batch result:
import { useApiBatch } from '@ametie/vue-muza-use'
const { data, execute } = useApiBatch(['/users/1', '/users/2'])
await execute()
for (const item of data.value) {
if (item.response) {
const rateLimit = item.response.headers['x-ratelimit-remaining']
console.log(`${item.url} — rate limit remaining: ${rateLimit}`)
}
if (item.error) {
console.error(`${item.url} — failed: ${item.error.message}`)
}
}
[!WARNING]
responseandrequestare new fields. If you were serializingBatchResultItemto JSON or storing it in a database, update your serialization logic to handle these new fields.
Cancelling a Batch — abort(), reset() and Re-Execute
A batch can be cancelled at any point: explicitly (abort() / reset()), by
calling execute() again while one is still running, or by the owning scope
being disposed (component unmount, store teardown).
Whatever the trigger, the cancelled run resolves — it never rejects and never returns a partially filled array:
const { data, execute, reset } = useApiBatch(urls)
const pending = execute()
reset() // e.g. from onUnmounted while requests are in flight
const results = await pending // always an array of `urls.length` items
results[0].status // 'success' if it had already landed, otherwise 'aborted'
Rules:
- Cancelled ≠ failed. Unanswered items come back as
status: "aborted"witherror: null. They never reacherrors, never fireonItemError, and never trigger the aggregatedBATCH_ALL_FAILEDerror — soerror.valuestaysnullwhen you simply navigate away. A cancelled SWR revalidation keeps its cached value indatawithstale: true. reset()and a replacingexecute()discard the run. After that it writes nothing more todata,errors,progressorloading, and itsonFinish,onItemSuccessandonItemErrorno longer fire — no toasts from a screen the user already left, no spinner turned off under the run that replaced it.abort()cancels without discarding. The run is still the current one, so it clearsloadingand callsonFinishwith its final snapshot once the requests unwind. Usereset()when you want the state gone as well.
Error Tolerance (Default)
By default, useApiBatch uses settled: true — failed requests don't stop the batch.
import { useApiBatch } from '@ametie/vue-muza-use'
interface User { id: number; name: string }
const {
successfulData,
errors,
progress,
execute
} = useApiBatch<User>([
'/users/1',
'/users/999',
'/users/3'
])
await execute()
console.log(successfulData.value.length) // 2
console.log(errors.value.length) // 1
With Progress Tracking
<script setup lang="ts">
import { useApiBatch } from '@ametie/vue-muza-use'
const urls = ['/users/1', '/users/2', '/users/3', '/users/4']
const { loading, progress, execute } = useApiBatch(urls, {
onProgress: (p) => {
console.log(`${p.percentage}% (${p.succeeded} ok, ${p.failed} failed)`)
}
})
</script>
<template>
<div v-if="loading">
<div class="progress-bar">
<div :style="{ width: progress.percentage + '%' }"></div>
</div>
<span>{{ progress.completed }} / {{ progress.total }}</span>
</div>
</template>
Reactive Requests — Auto-tracking
TL;DR: Pass a getter function as requests — the batch re-executes automatically when the getter's reactive dependencies change.
This mirrors useApi's auto-tracking behavior. No explicit watch option needed.
import { ref } from 'vue'
import { useApiBatch } from '@ametie/vue-muza-use'
interface User { id: number; name: string }
const pages = ref([1, 2, 3])
// Getter is auto-tracked — re-executes when pages.value changes
const { successfulData, loading } = useApiBatch<User>(
() => pages.value.map(page => ({ url: '/users', params: { page } }))
)
pages.value = [4, 5, 6] // → new batch fires automatically
Set lazy: true to disable auto-tracking and keep full manual control:
const { execute } = useApiBatch(
() => ids.value.map(id => `/items/${id}`),
{ lazy: true } // reactive changes to ids do NOT trigger re-execution
)
// You control when it runs
await execute()
Batch Polling
TL;DR: Re-execute the batch on a fixed interval — useful for dashboards and status monitoring.
Same semantics as useApi's poll option.
import { useApiBatch } from '@ametie/vue-muza-use'
const { data, loading } = useApiBatch(
['/stats/cpu', '/stats/memory', '/stats/disk'],
{
immediate: true,
poll: 5000 // re-execute every 5 seconds
}
)
With whenHidden control:
import { ref } from 'vue'
import { useApiBatch } from '@ametie/vue-muza-use'
const interval = ref(10000)
const { data, abort } = useApiBatch(
['/queue/jobs', '/queue/workers'],
{
immediate: true,
poll: { interval, whenHidden: false } // pauses when tab is hidden
}
)
interval.value = 0 // stop polling
⚙️ Advanced Configuration
Custom Axios Instance
TL;DR: Pass any pre-configured Axios instance — interceptors, timeouts, headers all work.
import axios from 'axios'
import { createApi } from '@ametie/vue-muza-use'
const customAxios = axios.create({
baseURL: 'https://api.example.com',
timeout: 30000,
headers: { 'X-Custom-Header': 'value' }
})
customAxios.interceptors.request.use((config) => {
config.headers['X-Request-ID'] = crypto.randomUUID()
return config
})
app.use(createApi({ axios: customAxios }))
Multiple Backends (Named Clients)
TL;DR: register extra axios instances under names and pick one per request with client. Each backend keeps its own baseURL, headers, timeout and error parser, and cache entries never mix. (1.9+)
import { createApi, createApiClient, createAuthSession } from '@ametie/vue-muza-use'
const session = createAuthSession({ refresh: { url: '/auth/refresh', baseURL: INSIGHTS_URL } })
app.use(createApi({
axios: createApiClient({ baseURL: INSIGHTS_URL, authSession: session }), // the "default" client
clients: {
tools: createApiClient({ baseURL: TOOLS_URL, authSession: session }), // shorthand
mcp: { axios: mcpApi, errorParser: parseMcpError }, // full form
},
}))
// api/useBrands.ts
export function useToolsBrands() {
return useApiGet<Brand[]>('/brands', { client: 'tools', immediate: true })
}
// Batch: batch-level client, overridable per item
useApiBatch(['/brands', { url: '/me', client: 'default' }], { client: 'tools' })
- Routing. A request without
client(or withclient: 'default') goes throughaxios, exactly as before. Withclient: 'tools'the request, its retries and its 401 refresh-retry go through thetoolsinstance, resolved against that instance'sbaseURL.clientis fixed per composable — it is not reactive and cannot be changed perexecute(). - Unknown names fail loudly.
client: 'nope'throws when the composable is created:Unknown API client "nope". Registered: default, tools, mcp. In a batch, an unknown item-level client fails that item only. The namedefaultis reserved —clients: { default: … }throws increateApi. - Auth. Each instance brings its own auth. Give them one auth session so a token expiry triggers a single refresh.
- Global options (
retry,cacheDefaults, global abort, devtools) apply to every client.
Type-checked client names
Out of the box client accepts any string. Declare your clients once to get autocomplete and typo errors:
// src/types/vue-muza-use.d.ts
import '@ametie/vue-muza-use'
declare module '@ametie/vue-muza-use' {
interface ApiClientRegistry {
tools: true
mcp: true
}
}
useApiGet('/brands', { client: 'tools' }) // ✅
useApiGet('/brands', { client: 'tool' }) // ❌ Type '"tool"' is not assignable to type … Did you mean '"tools"'?
Cache keys per client
Auto cache keys of a named client carry its name, so GET /brands on two backends are two entries:
| Request | Auto cache key |
|---|---|
useApi('/brands', { cache: true }) |
auto:GET:/brands:: (unchanged) |
useApi('/brands', { cache: true, client: 'tools' }) |
auto@tools:GET:/brands:: |
useApi('/brands', { cache: { id: 'brands' }, client: 'tools' }) |
brands (manual ids are used verbatim) |
invalidateCache({ prefix: 'auto@tools:GET:/brands' }) // only the tools entries
invalidateCache({ prefix: 'auto:GET:/brands' }) // only the default-client entries
Errors per client
A client's errorParser wins; without one the global errorParser is used, then the built-in parser. The global onError gets a third argument naming the client — for named clients only, so existing two-argument handlers see no change:
createApi({
axios: insightsApi,
clients: { tools: { axios: toolsApi, errorParser: parseToolsError } },
onError: (error, original, context) => {
toast.error(`[${context?.client ?? 'default'}] ${error.message}`)
},
})
Global Error Handler
TL;DR: Normalize errors from different backend formats in one place.
import { createApp } from 'vue'
import { createApi, createApiClient } from '@ametie/vue-muza-use'
import App from './App.vue'
const api = createApiClient({ baseURL: 'https://api.example.com' })
createApp(App).use(createApi({
axios: api,
onError: (error) => {
console.error(`[API Error] ${error.status}: ${error.message}`)
},
errorParser: (error: unknown) => {
const axiosError = error as {
response?: { data?: { message?: string; errors?: Record<string, string[]> }; status?: number }
message?: string
}
const response = axiosError.response?.data
if (response?.errors) {
return {
message: 'Validation Failed',
status: axiosError.response?.status ?? 422,
code: 'VALIDATION_ERROR',
errors: response.errors
}
}
return {
message: response?.message ?? axiosError.message ?? 'Unknown error',
status: axiosError.response?.status ?? 500,
details: error
}
},
globalOptions: {
retry: 2,
retryDelay: 1000,
retryStatusCodes: [408, 429, 500, 502, 503, 504],
useGlobalAbort: true
}
}))
globalOptions reference:
| Option | Type | Default | Description |
|---|---|---|---|
retry |
false | boolean | number |
false |
Default retry setting applied to all requests that don't specify their own |
retryDelay |
number |
1000 |
How many milliseconds to wait between retry attempts for all requests |
retryStatusCodes |
number[] |
[408,429,500,502,503,504] |
Default HTTP status codes that trigger a retry across all requests |
useGlobalAbort |
boolean |
true |
When true, all requests subscribe to the global abort controller |
refetchOnFocus |
boolean | { throttle?: number } |
undefined |
Apply refetchOnFocus to all useApi instances. Per-request value takes precedence (including false to opt-out) |
refetchOnReconnect |
boolean |
undefined |
Apply refetchOnReconnect to all useApi instances. Per-request value takes precedence (including false to opt-out) |
🔭 DevTools Panel
TL;DR: Enable it in the plugin and get a live network inspector in your browser. No extra packages to install.
The devtools panel is included with @ametie/vue-muza-use. It loads on demand and has zero impact on production bundles when disabled.
Setup
Pass devtools to createApi. Gate it on NODE_ENV to keep production builds clean:
import { createApp } from 'vue'
import { createApi, createApiClient } from '@ametie/vue-muza-use'
import App from './App.vue'
const api = createApiClient({ baseURL: 'https://api.example.com' })
createApp(App).use(createApi({
axios: api,
devtools: {
enabled: process.env.NODE_ENV !== 'production'
}
}))
The panel loads asynchronously — it has zero impact on startup time.
DevTools Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
— | Required. Set true to mount the panel |
maxHistory |
number |
300 |
Maximum number of requests kept in the Network tab history |
maxPayloadSize |
number |
200_000 |
Maximum bytes per payload/response before truncation in the viewer |
tabs |
DevtoolsTab[] |
[] |
Additional custom tabs to register in the panel |
🔐 Authentication & Token Management
Note: Authentication setup is optional. Only add this if your API requires JWT tokens.
Basic Auth Setup
TL;DR: Add withAuth: true and a refreshUrl to get automatic token injection and refresh.
import { createApiClient } from '@ametie/vue-muza-use'
const api = createApiClient({
baseURL: 'https://api.example.com',
withAuth: true,
authOptions: {
refreshUrl: '/auth/refresh',
onTokenRefreshFailed: () => {
window.location.href = '/login'
}
}
})
The library automatically:
- Injects
Authorization: Bearer <token>header - Refreshes expired tokens
- Queues requests during token refresh
- Retries failed requests after refresh
Token Management Modes
Mode 1: localStorage (Default)
import { createApiClient } from '@ametie/vue-muza-use'
const api = createApiClient({
baseURL: 'https://api.example.com',
authOptions: {
refreshUrl: '/auth/refresh',
onTokenRefreshFailed: () => router.push('/login')
}
})
Storage: Both accessToken and refreshToken in localStorage
Security: ⚠️ Vulnerable to XSS attacks
Use case: Development, internal tools
Mode 2: httpOnly Cookies (Production)
import { createApiClient } from '@ametie/vue-muza-use'
const api = createApiClient({
baseURL: 'https://api.example.com',
authOptions: {
refreshUrl: '/auth/refresh',
refreshWithCredentials: true,
onTokenRefreshFailed: () => router.push('/login')
}
})
Storage: Only accessToken in localStorage, refreshToken in httpOnly cookie
Security: 🔒 Protected from XSS attacks
Backend requirement: Must set Set-Cookie with HttpOnly; Secure; SameSite
withCredentials — Per-Request Cookie Control
TL;DR: Override the Axios instance default for a single request without changing global settings.
withCredentials controls whether cookies and other credentials are included in cross-origin requests (CORS). Set it globally in createApiClient and override it per request when needed.
// Global: withCredentials: false (Bearer token auth, no cookies)
const api = createApiClient({ baseURL: '/api' })
// Override: this specific endpoint needs cookies
const { data } = useApi('/user/session', {
withCredentials: true,
immediate: true,
})
// Global: withCredentials: true (full cookie-based auth)
const api = createApiClient({ baseURL: '/api', withCredentials: true })
// Override: skip cookies for a public CDN request
const { data } = useApi('https://cdn.example.com/config.json', {
withCredentials: false,
immediate: true,
})
Omitting withCredentials in useApi options means the Axios instance default is used — no override applied.
Saving Tokens After Login
import { useApi, tokenManager } from '@ametie/vue-muza-use'
import { useRouter } from 'vue-router'
interface LoginResponse {
accessToken: string
refreshToken: string
expiresIn: number
}
const router = useRouter()
const { execute } = useApi<LoginResponse>('/auth/login', {
method: 'POST',
authMode: 'public',
onSuccess(response) {
tokenManager.setTokens({
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken,
expiresIn: response.data.expiresIn
})
router.push('/dashboard')
}
})
authMode — Controlling Auth Per Request
TL;DR: Control whether a request includes auth tokens and whether it retries on 401.
| Value | Token sent? | Retries on 401? | Use case |
|---|---|---|---|
'default' |
✅ Always | ✅ Yes | Protected endpoints (most requests) |
'public' |
❌ Never | ❌ No | Login, registration, public content |
'optional' |
✅ If available | ❌ No | Content that works for guests and logged-in users |
import { ref } from 'vue'
import { useApi } from '@ametie/vue-muza-use'
interface Credentials { email: string; password: string }
interface Post { id: number; title: string }
const credentials = ref<Credentials>({ email: '', password: '' })
// Login — never send a token here
const { execute: login } = useApi('/auth/login', {
method: 'POST',
authMode: 'public',
data: credentials
})
// Public blog that shows extra content when logged in
const { data: posts } = useApi<Post[]>('/posts', {
authMode: 'optional',
immediate: true
})
tokenManager — Manual Token Control
TL;DR: Use this when you need to read, set, or clear tokens from outside a request.
The library manages tokens automatically. You only need tokenManager directly for:
- Saving tokens after login
- Clearing tokens on logout
- Checking if the user is currently logged in
Full API reference:
| Method | Returns | What it does |
|---|---|---|
getAccessToken() |
string | null |
The current access token, or null if not set |
getRefreshToken() |
string | null |
The refresh token, or null if using httpOnly cookies |
setTokens({ accessToken, refreshToken?, expiresIn? }) |
void |
Save new tokens after a successful login |
clearTokens() |
void |
Remove all tokens (call on logout) |
hasTokens() |
boolean |
true if an access token exists |
isTokenExpired() |
boolean |
true if the token has expired (5-second safety buffer applied) |
getTokenExpiresAt() |
number | null |
Unix timestamp (ms) when the current token expires |
getAuthHeader() |
string | null |
"Bearer <token>" string ready for use in headers, or null |
After login — save tokens:
import { tokenManager } from '@ametie/vue-muza-use'
import { useRouter } from 'vue-router'
const router = useRouter()
function onLoginSuccess(response: {
accessToken: string
refreshToken: string
expiresIn: number
}) {
tokenManager.setTokens({
accessToken: response.accessToken,
refreshToken: response.refreshToken,
expiresIn: response.expiresIn
})
router.push('/dashboard')
}
On logout — clear tokens:
import { tokenManager } from '@ametie/vue-muza-use'
import { useRouter } from 'vue-router'
const router = useRouter()
function logout() {
tokenManager.clearTokens()
router.push('/login')
}
Router guard — check before navigating:
import { tokenManager } from '@ametie/vue-muza-use'
import { createRouter } from 'vue-router'
const router = createRouter({ /* routes */ } as never)
router.beforeEach((to) => {
if (to.meta.requiresAuth && !tokenManager.hasTokens()) {
return '/login'
}
})
A second token store
TL;DR: When one backend only accepts a token that another backend mints, keep that token in its own store with its own localStorage keys.
tokenManager holds the app's main token under the keys accessToken, refreshToken and tokenExpiresAt. A second token needs a second store — otherwise saving it overwrites the main token, and every request to the main backend goes out with the wrong one:
import { TokenManager, LocalStorageTokenStorage } from '@ametie/vue-muza-use'
export const partnerTokens = new TokenManager(
new LocalStorageTokenStorage({
keys: {
accessToken: 'partner:accessToken',
refreshToken: 'p
Related Packages
Instance-scoped Axios orchestration and headless CRUD composables for Vue 3
Um plugin e composable para Vue 3 que simplifica chamadas de API com Axios, incl...
Control your API calls by using an amazing component which supports axios and vu...
A powerful and flexible API client for Vue 3 applications, built with TypeScript...
Decorator-driven, plugin-first HTTP client built on axios. Nest.js-style decorat...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.10.0 | ^3.5.0 | MIT | |
| 1.9.0 | ^3.5.0 | MIT | |
| 1.8.3 | ^3.5.0 | MIT | |
| 1.8.2 | ^3.5.0 | MIT | |
| 1.8.1 | ^3.5.0 | MIT | |
| 1.8.0 | ^3.5.0 | MIT | |
| 1.7.1 | ^3.5.0 | MIT | |
| 1.7.0 | ^3.5.0 | MIT | |
| 1.6.3 | ^3.5.0 | MIT | |
| 1.6.2 | ^3.5.0 | MIT | |
| 1.6.1 | ^3.5.0 | MIT | |
| 1.6.0 | ^3.5.0 | MIT | |
| 1.5.6 | ^3.5.0 | MIT | |
| 1.5.5 | ^3.5.0 | MIT | |
| 1.5.4 | ^3.5.0 | MIT | |
| 1.5.3 | ^3.5.0 | MIT | |
| 1.5.2 | ^3.5.0 | MIT | |
| 1.5.1 | ^3.5.0 | MIT | |
| 1.5.0 | ^3.5.0 | MIT | |
| 1.4.0 | ^3.5.0 | MIT | |
| 1.3.0 | ^3.5.0 | MIT | |
| 1.2.6 | ^3.5.0 | MIT | |
| 1.2.5 | ^3.5.0 | MIT | |
| 1.2.4 | ^3.5.0 | MIT | |
| 1.2.3 | ^3.5.0 | MIT | |
| 1.2.2 | ^3.5.0 | MIT | |
| 1.2.1 | ^3.5.0 | MIT | |
| 1.2.0 | ^3.5.0 | MIT | |
| 1.1.1 | ^3.5.0 | MIT | |
| 1.0.0 | ^3.5.0 | MIT |
Showing the latest 30 of 59. See every release on npm