@ametie/vue-muza-use 0.4.9 โ†’ 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,18 +13,48 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
13
13
 
14
14
  ## โœจ Features
15
15
 
16
+ **Core Features** (Get started in minutes):
16
17
  - ๐ŸŽฏ **Fully Type-Safe** โ€” End-to-end TypeScript support with strict typing for requests and responses
17
18
  - ๐Ÿ”„ **Smart Reactivity** โ€” Watch refs and automatically refetch when dependencies change
18
19
  - โฑ๏ธ **Built-in Debouncing** โ€” Perfect for search inputs and auto-save forms
19
20
  - ๐Ÿ›ก๏ธ **Race Condition Protection** โ€” Global abort controller cancels stale requests automatically
20
- - ๐Ÿ” **JWT Token Management** โ€” Automatic token refresh with request queueing on 401 responses
21
- - โ™ป๏ธ **Intelligent Retries** โ€” Lifecycle-aware retry logic that respects component unmounting
22
21
  - ๐Ÿ“Š **Auto-Polling** โ€” Built-in interval fetching with smart tab visibility detection
23
22
  - ๐Ÿงน **Zero Memory Leaks** โ€” Automatic cleanup of pending requests on component unmount
23
+
24
+ **Advanced Features** (When you need them):
25
+ - โ™ป๏ธ **Intelligent Retries** โ€” Lifecycle-aware retry logic that respects component unmounting
26
+ - ๐Ÿ” **JWT Token Management** โ€” Automatic token refresh with request queueing on 401 responses
24
27
  - ๐ŸŽ›๏ธ **Flexible Architecture** โ€” Bring your own Axios instance with full configuration control
25
28
 
26
29
  ---
27
30
 
31
+ ## ๐Ÿ“– Table of Contents
32
+
33
+ **Getting Started:**
34
+ - [Installation](#-installation)
35
+ - [Quick Start](#-quick-start)
36
+ - [Basic Usage](#-basic-usage)
37
+
38
+ **Core Features:**
39
+ - [Watch & Auto-Refetch](#watch--auto-refetch)
40
+ - [Polling](#polling-background-updates)
41
+ - [Error Handling](#error-handling)
42
+ - [Loading States](#loading-states)
43
+ - [Manual Data Updates](#manual-data-updates)
44
+
45
+ **Real-World Examples:**
46
+ - [Data Table with Pagination](#data-table-with-pagination--sorting)
47
+ - [Request Cancellation](#request-cancellation)
48
+
49
+ **Advanced:**
50
+ - [Custom Axios Instance](#-advanced-configuration)
51
+ - [Authentication & Tokens](#-authentication--token-management) *(Optional)*
52
+ - [API Reference](#-api-reference)
53
+
54
+ > ๐Ÿ’ก **New to the library?** Start with [Quick Start](#-quick-start), then explore [Basic Usage](#-basic-usage). Skip authentication until you need it!
55
+
56
+ ---
57
+
28
58
  ## ๐Ÿ“ฆ Installation
29
59
 
30
60
  ```bash
@@ -40,114 +70,10 @@ yarn add @ametie/vue-muza-use axios
40
70
 
41
71
  ---
42
72
 
43
- ## ๐Ÿ” Token Management
44
-
45
- The library supports **automatic token refresh** with two security modes:
46
-
47
- ### Mode 1: localStorage (Default)
48
- ```typescript
49
- const api = createApiClient({
50
- baseURL: 'https://api.example.com',
51
- authOptions: {
52
- refreshUrl: '/auth/refresh',
53
- refreshWithCredentials: false, // or omit (default)
54
- onTokenRefreshFailed: () => router.push('/login')
55
- }
56
- })
57
- ```
58
- - โœ… Both `accessToken` and `refreshToken` stored in localStorage
59
- - โš ๏ธ Less secure (vulnerable to XSS), but easier to implement
60
-
61
- ### Mode 2: httpOnly Cookies (Recommended for Production)
62
- ```typescript
63
- const api = createApiClient({
64
- baseURL: 'https://api.example.com',
65
- authOptions: {
66
- refreshUrl: '/auth/refresh',
67
- refreshWithCredentials: true, // ๐Ÿ”‘ Key parameter!
68
- onTokenRefreshFailed: () => router.push('/login')
69
- }
70
- })
71
- ```
72
- - โœ… Only `accessToken` in localStorage
73
- - ๐Ÿ”’ `refreshToken` sent via httpOnly cookie (XSS protection)
74
- - ๐Ÿ“ก Backend must set: `Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=None` (for cross-origin)
75
- - ๐ŸŽฏ **Smart Auto-Detection**: If no `refreshToken` in localStorage, `withCredentials: true` is automatically enabled
76
-
77
- **โš ๏ธ Common Issues & Solutions:**
78
-
79
- 1. **Cookie not sent to backend?**
80
- - Check: Browser DevTools โ†’ Application โ†’ Cookies
81
- - Cookie must have: `HttpOnly`, `Secure` (for HTTPS), `SameSite=None` (for cross-origin)
82
- - Cookie domain must match your request domain
83
-
84
- 2. **CORS errors?**
85
- - Backend must set: `Access-Control-Allow-Credentials: true`
86
- - Backend must set specific origin (NOT `*`): `Access-Control-Allow-Origin: https://your-frontend.com`
87
- - Frontend baseURL must match backend domain
88
-
89
- 3. **401 on refresh?**
90
- - Check Network tab โ†’ Refresh request โ†’ Headers
91
- - Verify "Cookie" header includes your refresh token
92
- - Backend logs: does it receive the cookie?
93
-
94
- ### Custom Refresh Payload
95
- Send additional data with refresh requests:
96
-
97
- **โš ๏ธ IMPORTANT:** Use functions for dynamic data (like tokens from storage):
98
-
99
- ```typescript
100
- const api = createApiClient({
101
- baseURL: 'https://api.example.com',
102
- authOptions: {
103
- refreshUrl: '/auth/refresh',
104
- // โŒ WRONG - computed once at app start
105
- // refreshPayload: { refreshToken: tokenManager.getRefreshToken() }
106
-
107
- // โœ… CORRECT - computed on each refresh
108
- refreshPayload: () => ({
109
- refreshToken: tokenManager.getRefreshToken(),
110
- timestamp: Date.now()
111
- })
112
- }
113
- })
114
- ```
115
-
116
- Static payload (for constants only):
117
- ```typescript
118
- refreshPayload: {
119
- deviceId: 'mobile-v1',
120
- platform: 'ios' // constants that don't change
121
- }
122
- ```
123
-
124
- Dynamic function:
125
- ```typescript
126
- refreshPayload: () => ({
127
- timestamp: Date.now(),
128
- sessionId: getSessionId() // reads current value
129
- })
130
- ```
131
-
132
- ### Saving Tokens After Login
133
- ```typescript
134
- import { tokenManager } from '@ametie/vue-muza-use'
135
-
136
- const { execute } = useApiPost<AuthResponse>('/auth/login', {
137
- authMode: 'public',
138
- onSuccess(response) {
139
- tokenManager.setTokens({
140
- accessToken: response.data.accessToken,
141
- refreshToken: response.data.refreshToken, // optional in cookie mode
142
- expiresIn: response.data.expiresIn
143
- })
144
- }
145
- })
146
- ```
147
- ---
148
-
149
73
  ## ๐Ÿš€ Quick Start
150
74
 
75
+ Get started in 2 minutes with minimal configuration.
76
+
151
77
  ### 1. Setup Plugin (`main.ts`)
152
78
 
153
79
  ```typescript
@@ -157,30 +83,20 @@ import App from './App.vue'
157
83
 
158
84
  const app = createApp(App)
159
85
 
160
- // Create configured Axios instance
86
+ // Create API client with minimal config
161
87
  const api = createApiClient({
162
- baseURL: 'https://api.example.com',
163
- withAuth: true, // Automatic token injection
164
- authOptions: {
165
- refreshUrl: '/auth/refresh',
166
- onTokenRefreshFailed: () => {
167
- window.location.href = '/login'
168
- }
169
- }
88
+ baseURL: 'https://api.example.com'
170
89
  })
171
90
 
172
91
  // Install plugin
173
- app.use(createApi({
174
- axios: api,
175
- onError: (error) => {
176
- console.error('API Error:', error.message)
177
- }
178
- }))
92
+ app.use(createApi({ axios: api }))
179
93
 
180
94
  app.mount('#app')
181
95
  ```
182
96
 
183
- ### 2. Simple GET Request
97
+ > ๐Ÿ’ก **That's it!** No auth configuration needed to get started. Add it later when you need it.
98
+
99
+ ### 2. Your First Request
184
100
 
185
101
  ```vue
186
102
  <script setup lang="ts">
@@ -193,7 +109,7 @@ interface User {
193
109
  }
194
110
 
195
111
  const { data, loading, error } = useApi<User>('/users/1', {
196
- immediate: true
112
+ immediate: true // Auto-fetch on mount
197
113
  })
198
114
  </script>
199
115
 
@@ -207,29 +123,23 @@ const { data, loading, error } = useApi<User>('/users/1', {
207
123
  </template>
208
124
  ```
209
125
 
210
- ### 3. Real-World Example: Live Search
126
+ ### 3. Live Search with Debounce
211
127
 
212
- This is where the library shines. Use a **getter function** for dynamic URLs, watch a ref, debounce input, and handle race conditions automatically:
128
+ This example shows the library's power: **automatic race condition handling** and **debouncing** built-in.
213
129
 
214
130
  ```vue
215
131
  <script setup lang="ts">
216
132
  import { ref } from 'vue'
217
133
  import { useApi } from '@ametie/vue-muza-use'
218
134
 
219
- interface Product {
220
- id: number
221
- name: string
222
- price: number
223
- }
224
-
225
135
  const searchQuery = ref('')
226
136
 
227
- // ๐Ÿ’ก Pass a getter function - no need for computed!
228
- const { data, loading } = useApi<Product[]>(
137
+ // ๐Ÿ’ก Use a getter function for dynamic URLs
138
+ const { data, loading } = useApi(
229
139
  () => `/products/search?q=${searchQuery.value}`,
230
140
  {
231
- watch: searchQuery, // โšก๏ธ Auto-refetch when query changes
232
- debounce: 500 // โฑ๏ธ Wait 500ms after typing stops
141
+ watch: searchQuery, // Auto-refetch when query changes
142
+ debounce: 500 // Wait 500ms after typing stops
233
143
  }
234
144
  )
235
145
  </script>
@@ -247,274 +157,145 @@ const { data, loading } = useApi<Product[]>(
247
157
  </template>
248
158
  ```
249
159
 
160
+ **๐ŸŽฏ What just happened?**
161
+ - No race conditions โ€” previous searches auto-cancel
162
+ - Debounce โ€” waits for user to stop typing
163
+ - TypeScript โ€” full type safety
164
+ - Clean code โ€” no manual cleanup needed
165
+
250
166
  ---
251
167
 
252
- ## ๐Ÿ“– Core Usage
168
+ ## ๐Ÿ“– Basic Usage
253
169
 
254
170
  ### GET Requests
255
171
 
256
- #### Basic Fetch
172
+ #### Manual Execution
257
173
  ```typescript
258
174
  const { data, loading, error, execute } = useApi<User>('/users/1')
259
175
 
260
- // Manually trigger
176
+ // Trigger manually (e.g., on button click)
261
177
  await execute()
262
178
  ```
263
179
 
264
180
  #### Auto-Fetch on Mount
265
181
  ```typescript
266
- useApi<User>('/users/1', {
267
- immediate: true,
268
- retry: 3, // Retry 3 times on network failure
269
- retryDelay: 1000 // Wait 1s between retries
270
- })
271
- ```
272
-
273
- #### Dynamic URLs
274
- Use a **getter function** for reactive URLs - simple and efficient:
275
-
276
- ```typescript
277
- const userId = ref(1)
278
-
279
- // โœ… Preferred: Getter function (no computed needed!)
280
- const { data } = useApi(() => `/users/${userId.value}`, {
281
- watch: userId,
282
- immediate: true
182
+ const { data } = useApi<User>('/users/1', {
183
+ immediate: true // Fetches automatically
283
184
  })
284
-
285
- // Also works: computed ref
286
- const url = computed(() => `/users/${userId.value}`)
287
- const { data } = useApi(url, { watch: userId, immediate: true })
288
185
  ```
289
186
 
290
- #### Query Parameters
291
- Pass `params` as static object or reactive ref:
292
-
187
+ #### With Query Parameters
293
188
  ```typescript
294
189
  const filters = ref({
295
190
  status: 'active',
296
- sort: 'name',
297
191
  limit: 20
298
192
  })
299
193
 
300
194
  const { data } = useApi('/users', {
301
- params: filters, // Automatically unwrapped
302
- watch: filters, // Re-fetch when filters change
303
- debounce: 300
195
+ params: filters, // Automatically unwrapped
196
+ watch: filters, // Re-fetch when filters change
197
+ immediate: true
304
198
  })
305
-
306
- // URL becomes: /users?status=active&sort=name&limit=20
307
199
  ```
308
200
 
309
201
  ---
310
202
 
311
203
  ### POST/PUT/PATCH Requests
312
204
 
313
- #### Form Submission with Loading State
205
+ #### Simple Form Submission
314
206
  ```vue
315
207
  <script setup lang="ts">
316
208
  import { ref } from 'vue'
317
209
  import { useApi } from '@ametie/vue-muza-use'
318
210
 
319
- interface LoginForm {
320
- email: string
321
- password: string
322
- }
323
-
324
- interface LoginResponse {
325
- token: string
326
- user: { id: number; name: string }
327
- }
328
-
329
- const form = ref<LoginForm>({
211
+ const form = ref({
330
212
  email: '',
331
213
  password: ''
332
214
  })
333
215
 
334
- const { execute, loading, error } = useApi<LoginResponse>('/auth/login', {
216
+ const { execute, loading, error } = useApi('/auth/login', {
335
217
  method: 'POST',
336
- authMode: 'public', // ๐Ÿ‘ˆ Skip token injection for login
337
- data: form, // Ref is auto-unwrapped
218
+ data: form, // Ref is auto-unwrapped
338
219
  onSuccess: (response) => {
339
- localStorage.setItem('token', response.data.token)
340
- router.push('/dashboard')
220
+ console.log('Logged in!', response.data)
341
221
  }
342
222
  })
343
-
344
- const handleSubmit = async () => {
345
- await execute()
346
- }
347
223
  </script>
348
224
 
349
225
  <template>
350
- <form @submit.prevent="handleSubmit">
351
- <input v-model="form.email" type="email" :disabled="loading" />
352
- <input v-model="form.password" type="password" :disabled="loading" />
353
-
354
- <button type="submit" :disabled="loading">
226
+ <form @submit.prevent="execute">
227
+ <input v-model="form.email" type="email" />
228
+ <input v-model="form.password" type="password" />
229
+ <button :disabled="loading">
355
230
  {{ loading ? 'Signing in...' : 'Sign In' }}
356
231
  </button>
357
-
358
- <p v-if="error" class="error">{{ error.message }}</p>
232
+ <p v-if="error">{{ error.message }}</p>
359
233
  </form>
360
234
  </template>
361
235
  ```
362
236
 
363
- #### Update with Optimistic UI
364
- ```typescript
365
- const { execute: updateProfile } = useApi('/user/profile', {
366
- method: 'PUT',
367
- onBefore: () => {
368
- // Show optimistic update
369
- localProfile.value = { ...localProfile.value, ...changes }
370
- },
371
- onSuccess: (response) => {
372
- toast.success('Profile updated!')
373
- },
374
- onError: () => {
375
- // Rollback on error
376
- localProfile.value = originalProfile
377
- }
378
- })
379
- ```
380
-
381
237
  ---
382
238
 
383
- ### Auto-Refetching with Watch
239
+ ## ๐ŸŽฏ Core Features
384
240
 
385
- Watch reactive dependencies and auto-refetch when they change. Perfect for filters, search, and dynamic content.
241
+ ### Watch & Auto-Refetch
386
242
 
387
- #### Live Search (Debounced)
388
- ```vue
389
- <script setup lang="ts">
243
+ Watch refs and automatically refetch when they change. Perfect for filters, search, and dynamic content.
244
+
245
+ #### Single Dependency
246
+ ```typescript
247
+ const userId = ref(1)
248
+
249
+ const { data } = useApi(() => `/users/${userId.value}`, {
250
+ watch: userId,
251
+ immediate: true
252
+ })
253
+
254
+ // Change userId โ†’ automatic refetch
255
+ userId.value = 2
256
+ ```
257
+
258
+ #### Multiple Dependencies
259
+ ```typescript
390
260
  const searchQuery = ref('')
391
261
  const category = ref('all')
392
262
 
393
- // Use getter function for dynamic URL construction
394
- const { data, loading } = useApi<Product[]>(
263
+ const { data } = useApi(
395
264
  () => `/products?q=${searchQuery.value}&category=${category.value}`,
396
265
  {
397
- watch: [searchQuery, category], // Watch multiple refs
398
- debounce: 500 // Debounce search input
266
+ watch: [searchQuery, category],
267
+ debounce: 500
399
268
  }
400
269
  )
401
- </script>
402
-
403
- <template>
404
- <input v-model="searchQuery" placeholder="Search..." />
405
- <select v-model="category">
406
- <option value="all">All</option>
407
- <option value="electronics">Electronics</option>
408
- </select>
409
-
410
- <ProductList :products="data" :loading="loading" />
411
- </template>
412
- ```
413
-
414
- #### Data Table with Pagination & Sorting
415
- ```vue
416
- <script setup lang="ts">
417
- interface PaginatedResponse<T> {
418
- data: T[]
419
- total: number
420
- page: number
421
- }
422
-
423
- const page = ref(1)
424
- const sortBy = ref('created_at')
425
- const sortOrder = ref<'asc' | 'desc'>('desc')
426
-
427
- const params = computed(() => ({
428
- page: page.value,
429
- sort_by: sortBy.value,
430
- sort_order: sortOrder.value,
431
- per_page: 20
432
- }))
433
-
434
- const { data, loading } = useApi<PaginatedResponse<Order>>('/orders', {
435
- params,
436
- watch: params, // Auto-refetch when any param changes
437
- immediate: true
438
- })
439
-
440
- const handleSort = (column: string) => {
441
- if (sortBy.value === column) {
442
- sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
443
- } else {
444
- sortBy.value = column
445
- sortOrder.value = 'desc'
446
- }
447
- }
448
- </script>
449
-
450
- <template>
451
- <table>
452
- <thead>
453
- <tr>
454
- <th @click="handleSort('id')">ID</th>
455
- <th @click="handleSort('created_at')">Date</th>
456
- <th @click="handleSort('total')">Total</th>
457
- </tr>
458
- </thead>
459
- <tbody v-if="!loading">
460
- <tr v-for="order in data?.data" :key="order.id">
461
- <td>{{ order.id }}</td>
462
- <td>{{ order.created_at }}</td>
463
- <td>${{ order.total }}</td>
464
- </tr>
465
- </tbody>
466
- </table>
467
-
468
- <Pagination
469
- v-model="page"
470
- :total="data?.total"
471
- :loading="loading"
472
- />
473
- </template>
474
270
  ```
475
271
 
476
272
  #### Auto-Save Form
477
273
  ```typescript
478
274
  const settings = ref({
479
275
  theme: 'dark',
480
- notifications: true,
481
- language: 'en'
276
+ notifications: true
482
277
  })
483
278
 
484
279
  useApi('/user/settings', {
485
280
  method: 'PUT',
486
281
  data: settings,
487
- watch: settings, // Deep watch by default
488
- debounce: 1000, // Save 1s after changes stop
489
- onSuccess: () => {
490
- toast.success('Settings saved')
491
- }
282
+ watch: settings, // Deep watch by default
283
+ debounce: 1000, // Save 1s after changes stop
284
+ onSuccess: () => toast.success('Saved!')
492
285
  })
493
286
  ```
494
287
 
495
288
  ---
496
289
 
497
- ### Auto-Polling (Background Updates)
290
+ ### Polling (Background Updates)
498
291
 
499
- Keep data fresh with intelligent polling. Automatically pauses when tab is hidden (configurable).
292
+ Keep data fresh with smart polling. Automatically pauses when browser tab is hidden.
500
293
 
501
294
  #### Simple Polling
502
295
  ```typescript
503
- // Fetch notifications every 5 seconds
504
- const { data } = useApi<Notification[]>('/notifications', {
505
- immediate: true,
506
- poll: 5000
507
- })
508
- ```
509
-
510
- #### Smart Polling with Controls
511
- ```typescript
512
- const { data, loading } = useApi('/stock-prices', {
296
+ const { data } = useApi('/notifications', {
513
297
  immediate: true,
514
- poll: {
515
- interval: 1000,
516
- whenHidden: false // โš ๏ธ Pause when tab is hidden (default)
517
- }
298
+ poll: 5000 // Fetch every 5 seconds
518
299
  })
519
300
  ```
520
301
 
@@ -522,71 +303,249 @@ const { data, loading } = useApi('/stock-prices', {
522
303
  ```typescript
523
304
  const pollInterval = ref(3000)
524
305
 
525
- // Start/stop polling by changing the ref
526
306
  const { data } = useApi('/live-feed', {
527
307
  poll: pollInterval,
528
308
  immediate: true
529
309
  })
530
310
 
531
311
  // Stop polling
532
- const stopPolling = () => pollInterval.value = 0
312
+ pollInterval.value = 0
533
313
 
534
- // Resume with 5s interval
535
- const startPolling = () => pollInterval.value = 5000
314
+ // Resume with different interval
315
+ pollInterval.value = 5000
536
316
  ```
537
317
 
538
318
  ---
539
319
 
540
- ### Race Condition Prevention
320
+ ### Error Handling
541
321
 
542
- #### Global Abort Controller
543
- Cancel all pending requests in a scope when filters change. By default, all requests subscribe to the global abort controller:
322
+ #### Per-Request Error Handling
323
+ ```typescript
324
+ const { error, execute } = useApi('/users', {
325
+ onError: (error) => {
326
+ if (error.status === 404) {
327
+ toast.error('User not found')
328
+ } else {
329
+ toast.error('Something went wrong')
330
+ }
331
+ },
332
+ skipErrorNotification: true // Skip global handler
333
+ })
334
+ ```
544
335
 
545
- ```vue
546
- <script setup lang="ts">
547
- import { useAbortController } from '@ametie/vue-muza-use'
336
+ #### Retry on Failure
337
+ ```typescript
338
+ useApi('/flaky-endpoint', {
339
+ immediate: true,
340
+ retry: 3, // Retry 3 times
341
+ retryDelay: 1000 // Wait 1s between retries
342
+ })
343
+ ```
548
344
 
549
- const filters = ref({ category: 'all', priceMin: 0, priceMax: 1000 })
345
+ ---
550
346
 
551
- // ๐Ÿ’ก useGlobalAbort: true by default - automatically subscribed
552
- const { data: products } = useApi('/products', {
553
- params: filters
554
- })
347
+ ### Loading States
348
+
349
+ #### Per-Request Loading
350
+ ```typescript
351
+ const { data: user, loading: userLoading } = useApi('/user')
352
+ const { data: posts, loading: postsLoading } = useApi('/posts')
353
+
354
+ // Each request tracks its own loading state
355
+ ```
555
356
 
556
- const { data: stats } = useApi('/products/stats', {
557
- params: filters
357
+ #### Lifecycle Hooks
358
+ ```typescript
359
+ const { execute } = useApi('/analytics', {
360
+ onBefore: () => {
361
+ loadingBar.start()
362
+ },
363
+ onSuccess: (response) => {
364
+ console.log('Success!', response.data)
365
+ },
366
+ onError: (error) => {
367
+ console.error('Failed:', error.message)
368
+ },
369
+ onFinish: () => {
370
+ loadingBar.finish() // Always called
371
+ }
558
372
  })
373
+ ```
559
374
 
560
- const { abortAll } = useAbortController()
375
+ ---
561
376
 
562
- const resetFilters = () => {
563
- abortAll() // ๐Ÿ›‘ Cancel both pending requests
564
- filters.value = { category: 'all', priceMin: 0, priceMax: 1000 }
377
+ ### Manual Data Updates
378
+
379
+ Use `setData` to manually update the data ref. Supports direct values or updater functions (like React's `setState`).
380
+
381
+ > ๐ŸŽ“ **When to use `setData`:**
382
+ > โœ… Adding/removing/updating items in arrays
383
+ > โœ… Local sorting/filtering (without refetching)
384
+ > โœ… Transform data in `onSuccess` (adding computed fields)
385
+ >
386
+ > **When to use `computed` instead:**
387
+ > โœ… Completely changing data structure (e.g., API format โ†’ App format)
388
+ > โœ… Extracting nested data that changes the return type
389
+ > โœ… Complex transformations that depend on other refs
390
+
391
+ #### Add/Remove/Update Items
392
+ ```typescript
393
+ const { data, setData } = useApi<Todo[]>('/todos', { immediate: true })
394
+
395
+ // Add item
396
+ const addTodo = (newTodo: Todo) => {
397
+ setData(prev => prev ? [...prev, newTodo] : [newTodo])
398
+ }
399
+
400
+ // Remove item
401
+ const removeTodo = (id: number) => {
402
+ setData(prev => prev?.filter(t => t.id !== id) ?? null)
403
+ }
404
+
405
+ // Update item
406
+ const updateTodo = (id: number, updates: Partial<Todo>) => {
407
+ setData(prev =>
408
+ prev?.map(t => t.id === id ? { ...t, ...updates } : t) ?? null
409
+ )
410
+ }
411
+ ```
412
+
413
+ #### Sort/Filter Locally
414
+ ```typescript
415
+ const { data, setData } = useApi<Product[]>('/products', { immediate: true })
416
+
417
+ const sortByPrice = () => {
418
+ setData(prev => prev ? [...prev].sort((a, b) => a.price - b.price) : null)
419
+ }
420
+
421
+ const filterActive = () => {
422
+ setData(prev => prev?.filter(p => p.active) ?? null)
565
423
  }
566
424
 
567
- // To opt-out of global abort:
568
- const { data: independent } = useApi('/independent-request', {
569
- useGlobalAbort: false // This request won't be cancelled by abortAll()
425
+ // Reset to original
426
+ const resetFilters = () => execute()
427
+ ```
428
+
429
+ #### Transform in `onSuccess`
430
+
431
+ Use `setData` in `onSuccess` to transform data right after fetching. Two approaches:
432
+
433
+ **Approach 1: Same type (recommended)**
434
+ ```typescript
435
+ interface User {
436
+ id: number
437
+ firstName: string
438
+ lastName: string
439
+ fullName?: string // Optional field
440
+ }
441
+
442
+ const { data, setData } = useApi<User[]>('/users', {
443
+ immediate: true,
444
+ onSuccess: ({ data: users }) => {
445
+ // Add computed field - still User[] type
446
+ setData(users.map(u => ({
447
+ ...u,
448
+ fullName: `${u.firstName} ${u.lastName}`
449
+ })))
450
+ }
451
+ })
452
+ ```
453
+
454
+ **Approach 2: Different structure (use separate computed)**
455
+ ```typescript
456
+ interface ApiUser {
457
+ first_name: string
458
+ last_name: string
459
+ }
460
+
461
+ // If API returns different structure, use computed for transformation
462
+ const { data: rawData } = useApi<ApiUser[]>('/users', { immediate: true })
463
+
464
+ const users = computed(() =>
465
+ rawData.value?.map(u => ({
466
+ firstName: u.first_name,
467
+ lastName: u.last_name,
468
+ fullName: `${u.first_name} ${u.last_name}`
469
+ })) ?? []
470
+ )
471
+ ```
472
+
473
+ > ๐Ÿ’ก **Rule of thumb:**
474
+ > - โœ… **Use `setData` in `onSuccess`** if you're adding/modifying fields but keeping the same base type
475
+ > - โœ… **Use `computed`** if you're completely changing the data structure (e.g., snake_case โ†’ camelCase)
476
+
477
+ ---
478
+
479
+ ## ๐Ÿ“Š Real-World Examples
480
+
481
+ ### Data Table with Pagination & Sorting
482
+ ```vue
483
+ <script setup lang="ts">
484
+ const page = ref(1)
485
+ const sortBy = ref('created_at')
486
+ const sortOrder = ref<'asc' | 'desc'>('desc')
487
+
488
+ const params = computed(() => ({
489
+ page: page.value,
490
+ sort_by: sortBy.value,
491
+ sort_order: sortOrder.value,
492
+ per_page: 20
493
+ }))
494
+
495
+ const { data, loading } = useApi('/orders', {
496
+ params,
497
+ watch: params,
498
+ immediate: true
570
499
  })
571
500
  </script>
501
+
502
+ <template>
503
+ <table>
504
+ <thead>
505
+ <tr>
506
+ <th @click="sortBy = 'id'">ID</th>
507
+ <th @click="sortBy = 'created_at'">Date</th>
508
+ <th @click="sortBy = 'total'">Total</th>
509
+ </tr>
510
+ </thead>
511
+ <tbody v-if="!loading">
512
+ <tr v-for="order in data?.data" :key="order.id">
513
+ <td>{{ order.id }}</td>
514
+ <td>{{ order.created_at }}</td>
515
+ <td>${{ order.total }}</td>
516
+ </tr>
517
+ </tbody>
518
+ </table>
519
+
520
+ <Pagination v-model="page" :total="data?.total" />
521
+ </template>
572
522
  ```
573
523
 
574
- #### Per-Request Abort
524
+
525
+ ### Request Cancellation
575
526
  ```typescript
576
- const { execute, abort } = useApi('/long-running-task')
527
+ import { useAbortController } from '@ametie/vue-muza-use'
577
528
 
578
- // Start
579
- execute()
529
+ const { abortAll } = useAbortController()
580
530
 
581
- // Cancel if needed
582
- setTimeout(() => abort('User cancelled'), 5000)
531
+ // Multiple requests
532
+ const { data: products } = useApi('/products', { params: filters })
533
+ const { data: stats } = useApi('/stats', { params: filters })
534
+
535
+ // Cancel all when filters reset
536
+ const resetFilters = () => {
537
+ abortAll() // ๐Ÿ›‘ Cancel both requests
538
+ filters.value = { /* defaults */ }
539
+ }
583
540
  ```
584
541
 
542
+ ---
543
+
585
544
  ## โš™๏ธ Advanced Configuration
586
545
 
587
546
  ### Custom Axios Instance
588
547
 
589
- If you need full control over Axios configuration, create your own instance:
548
+ Full control over Axios configuration:
590
549
 
591
550
  ```typescript
592
551
  import axios from 'axios'
@@ -595,14 +554,12 @@ import { createApi } from '@ametie/vue-muza-use'
595
554
  const customAxios = axios.create({
596
555
  baseURL: 'https://api.example.com',
597
556
  timeout: 30000,
598
- headers: {
599
- 'X-Custom-Header': 'value'
600
- }
557
+ headers: { 'X-Custom-Header': 'value' }
601
558
  })
602
559
 
603
- // Add your own interceptors
560
+ // Add custom interceptors
604
561
  customAxios.interceptors.request.use((config) => {
605
- // Custom logic
562
+ // Your logic
606
563
  return config
607
564
  })
608
565
 
@@ -611,42 +568,36 @@ app.use(createApi({ axios: customAxios }))
611
568
 
612
569
  ---
613
570
 
614
- ### Error Handling
615
-
616
- #### Custom Error Parser
571
+ ### Global Error Handler
617
572
 
618
- Every backend returns errors differently. Normalize them into a standard format:
573
+ Normalize errors from different backend formats:
619
574
 
620
575
  ```typescript
621
- import { createApi } from '@ametie/vue-muza-use'
622
-
623
576
  app.use(createApi({
624
577
  axios: api,
578
+
579
+ // Global error handler
580
+ onError: (error) => {
581
+ toast.error(error.message)
582
+ },
583
+
584
+ // Error parser (normalize backend responses)
625
585
  errorParser: (error: any) => {
626
586
  const response = error.response?.data
627
587
 
628
- // Laravel/Rails validation errors
588
+ // Laravel validation errors
629
589
  if (response?.errors) {
630
590
  return {
631
591
  message: 'Validation Failed',
632
592
  status: error.response.status,
633
593
  code: 'VALIDATION_ERROR',
634
- errors: response.errors // { email: ['Invalid email format'] }
635
- }
636
- }
637
-
638
- // Custom API format
639
- if (response?.error?.message) {
640
- return {
641
- message: response.error.message,
642
- status: error.response?.status || 500,
643
- code: response.error.code
594
+ errors: response.errors
644
595
  }
645
596
  }
646
597
 
647
- // Default fallback
598
+ // Default format
648
599
  return {
649
- message: error.message || 'An unexpected error occurred',
600
+ message: response?.message || error.message || 'Unknown error',
650
601
  status: error.response?.status || 500,
651
602
  details: error
652
603
  }
@@ -654,245 +605,180 @@ app.use(createApi({
654
605
  }))
655
606
  ```
656
607
 
657
- #### Using Errors in Components
658
-
659
- ```vue
660
- <script setup lang="ts">
661
- const { data, error, execute } = useApi('/users', {
662
- skipErrorNotification: true // Skip global error handler
663
- })
664
-
665
- // Access structured error
666
- if (error.value) {
667
- console.log(error.value.message) // User-friendly message
668
- console.log(error.value.status) // HTTP status code
669
- console.log(error.value.code) // Custom error code
670
- console.log(error.value.errors) // Validation errors
671
- }
672
- </script>
673
-
674
- <template>
675
- <div v-if="error" class="error-banner">
676
- {{ error.message }}
677
-
678
- <!-- Display validation errors -->
679
- <ul v-if="error.errors">
680
- <li v-for="(msgs, field) in error.errors" :key="field">
681
- <strong>{{ field }}:</strong> {{ msgs.join(', ') }}
682
- </li>
683
- </ul>
684
- </div>
685
- </template>
686
- ```
687
-
688
608
  ---
689
609
 
690
- ### Authentication & Token Management
691
-
692
- #### How Token Refresh Works
610
+ ## ๐Ÿ” Authentication & Token Management
693
611
 
694
- The library implements a **request queue pattern** to handle token expiration gracefully:
612
+ > **Note:** Authentication setup is optional. Only add this if your API requires JWT tokens.
695
613
 
696
- ```
697
- 1. Request fails with 401 Unauthorized
698
- โ†“
699
- 2. Add request to queue & pause all new requests
700
- โ†“
701
- 3. Attempt token refresh (POST to refreshUrl)
702
- โ†“
703
- 4. Success? โ†’ Replay all queued requests with new token
704
- Failure? โ†’ Call onTokenRefreshFailed() & reject queue
705
- ```
614
+ ### Basic Auth Setup
706
615
 
707
- This happens **transparently** โ€” your components just wait a bit longer for the response.
708
-
709
- #### Configuration
616
+ Add authentication to your API client:
710
617
 
711
618
  ```typescript
712
619
  const api = createApiClient({
713
620
  baseURL: 'https://api.example.com',
714
- withAuth: true,
621
+ withAuth: true, // Enable automatic token injection
715
622
  authOptions: {
716
- refreshUrl: '/auth/refresh', // Default: '/auth/refresh'
717
-
718
- // โœจ NEW: Handle successful refresh response
719
- onTokenRefreshed: (response) => {
720
- // Extract additional data from refresh response
721
- const { user, permissions } = response.data
722
-
723
- // Update app state
724
- store.commit('SET_USER', user)
725
- store.commit('SET_PERMISSIONS', permissions)
726
-
727
- // Analytics
728
- analytics.track('token_refreshed', { userId: user.id })
729
- },
730
-
623
+ refreshUrl: '/auth/refresh',
731
624
  onTokenRefreshFailed: () => {
732
- // Called when refresh fails (expired refresh token)
733
- localStorage.clear()
625
+ // Redirect to login when refresh fails
734
626
  window.location.href = '/login'
735
627
  }
736
628
  }
737
629
  })
738
630
  ```
739
631
 
740
- #### Token Storage
741
-
742
- The library expects you to manage token storage. Implement these utilities:
743
-
744
- ```typescript
745
- // utils/auth.ts
746
- export const getAccessToken = (): string | null => {
747
- return localStorage.getItem('access_token')
748
- }
749
-
750
- export const setAccessToken = (token: string): void => {
751
- localStorage.setItem('access_token', token)
752
- }
632
+ The library automatically:
633
+ - Injects `Authorization: Bearer <token>` header
634
+ - Refreshes expired tokens
635
+ - Queues requests during token refresh
636
+ - Retries failed requests after refresh
753
637
 
754
- export const clearTokens = (): void => {
755
- localStorage.removeItem('access_token')
756
- }
757
- ```
638
+ ---
758
639
 
759
- The `createApiClient` automatically injects `Authorization: Bearer <token>` on every request.
640
+ ### Token Management Modes
760
641
 
761
- #### Public Endpoints
642
+ #### Mode 1: localStorage (Default)
762
643
 
763
- For endpoints that don't require authentication (login, register, public data):
644
+ Simple setup for development or internal tools:
764
645
 
765
646
  ```typescript
766
- useApi('/auth/login', {
767
- method: 'POST',
768
- authMode: 'public', // ๐Ÿ‘ˆ Skips Authorization header
769
- data: credentials
647
+ const api = createApiClient({
648
+ baseURL: 'https://api.example.com',
649
+ authOptions: {
650
+ refreshUrl: '/auth/refresh',
651
+ onTokenRefreshFailed: () => router.push('/login')
652
+ }
770
653
  })
771
654
  ```
772
655
 
773
- #### Monitoring Auth Events
774
-
775
- Track authentication events for debugging or analytics:
776
-
777
- ```typescript
778
- import { setAuthMonitor } from '@ametie/vue-muza-use'
779
-
780
- setAuthMonitor((event, payload) => {
781
- console.log(`[Auth Event] ${event}`, payload)
782
-
783
- // Events: 'token_refresh_start', 'token_refresh_success',
784
- // 'token_refresh_failed', 'token_injected'
785
- })
786
- ```
656
+ **Storage:** Both `accessToken` and `refreshToken` in localStorage
657
+ **Security:** โš ๏ธ Vulnerable to XSS attacks
658
+ **Use case:** Development, internal tools
787
659
 
788
660
  ---
789
661
 
790
- ### Lifecycle Hooks
662
+ #### Mode 2: httpOnly Cookies (Production)
791
663
 
792
- Fine-grained control over request lifecycle:
664
+ Recommended for production apps with sensitive data:
793
665
 
794
666
  ```typescript
795
- const { execute } = useApi('/analytics', {
796
- onBefore: () => {
797
- console.log('Request starting...')
798
- loadingBar.start()
799
- },
800
- onSuccess: (response) => {
801
- console.log('Success!', response.data)
802
- analytics.track('api_success', { endpoint: '/analytics' })
803
- },
804
- onError: (error) => {
805
- console.error('Failed:', error.message)
806
- sentry.captureException(error)
807
- },
808
- onFinish: () => {
809
- console.log('Request finished (success or error)')
810
- loadingBar.finish()
667
+ const api = createApiClient({
668
+ baseURL: 'https://api.example.com',
669
+ authOptions: {
670
+ refreshUrl: '/auth/refresh',
671
+ refreshWithCredentials: true, // ๐Ÿ”‘ Send cookies for refresh
672
+ onTokenRefreshFailed: () => router.push('/login')
811
673
  }
812
674
  })
813
675
  ```
814
676
 
815
- **Hook Execution Order:**
816
- 1. `onBefore`
817
- 2. Request execution
818
- 3. `onSuccess` OR `onError`
819
- 4. `onFinish` (always runs)
677
+ **Storage:** Only `accessToken` in localStorage, `refreshToken` in httpOnly cookie
678
+ **Security:** ๐Ÿ”’ Protected from XSS attacks
679
+ **Backend requirement:** Must set `Set-Cookie` with `HttpOnly; Secure; SameSite`
820
680
 
821
- ---
681
+ **Common Issues:**
682
+ - **Cookie not sent?** Check cookie domain and `SameSite` attribute
683
+ - **CORS error?** Backend must set `Access-Control-Allow-Credentials: true`
684
+ - **401 on refresh?** Verify cookie is included in request headers
822
685
 
823
- ### Retry Logic
686
+ ---
824
687
 
825
- Intelligent retries with lifecycle awareness โ€” retries stop if component unmounts:
688
+ ### Saving Tokens After Login
826
689
 
827
690
  ```typescript
828
- useApi('/flaky-endpoint', {
829
- retry: 3, // Retry up to 3 times
830
- retryDelay: 1000, // Wait 1s between attempts
831
- onError: (error, attempt) => {
832
- console.log(`Attempt ${attempt} failed`)
691
+ import { tokenManager } from '@ametie/vue-muza-use'
692
+
693
+ const { execute } = useApi('/auth/login', {
694
+ method: 'POST',
695
+ authMode: 'public', // No auth for login endpoint
696
+ onSuccess(response) {
697
+ tokenManager.setTokens({
698
+ accessToken: response.data.accessToken,
699
+ refreshToken: response.data.refreshToken, // Optional in cookie mode
700
+ expiresIn: response.data.expiresIn
701
+ })
702
+
703
+ router.push('/dashboard')
833
704
  }
834
705
  })
835
706
  ```
836
707
 
837
- **When retries happen:**
838
- - Network errors (timeouts, connection refused)
839
- - 5xx server errors
840
- - Configurable via axios retry interceptor
841
-
842
- **When retries DON'T happen:**
843
- - 4xx client errors (bad request, unauthorized, etc.)
844
- - Component is unmounted (automatic cleanup)
845
-
846
708
  ---
847
709
 
848
- ### Global vs Local Loading States
710
+ ### Public Endpoints
711
+
712
+ Skip authentication for public endpoints:
849
713
 
850
- #### Local Loading (Per Request)
851
714
  ```typescript
852
- const { data: user, loading: userLoading } = useApi('/user')
853
- const { data: posts, loading: postsLoading } = useApi('/posts')
715
+ // Login (no auth needed)
716
+ useApi('/auth/login', {
717
+ method: 'POST',
718
+ authMode: 'public',
719
+ data: credentials
720
+ })
854
721
 
855
- // Each request has its own loading state
722
+ // Public blog posts
723
+ useApi('/blog/posts', {
724
+ authMode: 'public',
725
+ immediate: true
726
+ })
856
727
  ```
857
728
 
858
- #### Global Loading (Shared)
859
- ```typescript
860
- const globalLoading = ref(false)
729
+ ---
861
730
 
862
- useApi('/user', {
863
- onBefore: () => globalLoading.value = true,
864
- onFinish: () => globalLoading.value = false
865
- })
731
+ ### Advanced: Custom Refresh Payload
732
+
733
+ Send additional data with token refresh requests:
866
734
 
867
- useApi('/posts', {
868
- onBefore: () => globalLoading.value = true,
869
- onFinish: () => globalLoading.value = false
735
+ ```typescript
736
+ const api = createApiClient({
737
+ baseURL: 'https://api.example.com',
738
+ authOptions: {
739
+ refreshUrl: '/auth/refresh',
740
+
741
+ // โš ๏ธ Use function for dynamic data
742
+ refreshPayload: () => ({
743
+ refreshToken: tokenManager.getRefreshToken(),
744
+ deviceId: getDeviceId(),
745
+ timestamp: Date.now()
746
+ })
747
+ }
870
748
  })
871
749
  ```
872
750
 
873
- #### Smart Loading (Debounced)
874
- Prevent loading flicker for fast responses:
751
+ ---
752
+
753
+ ### Advanced: Token Refresh Callback
754
+
755
+ Handle additional data from refresh response:
875
756
 
876
757
  ```typescript
877
- import { ref, watch } from 'vue'
878
-
879
- const loading = ref(false)
880
- const showLoading = ref(false)
881
- let timeout: ReturnType<typeof setTimeout>
882
-
883
- watch(loading, (val) => {
884
- if (val) {
885
- // Show loading after 200ms (skip if request is fast)
886
- timeout = setTimeout(() => {
887
- showLoading.value = true
888
- }, 200)
889
- } else {
890
- clearTimeout(timeout)
891
- showLoading.value = false
758
+ const api = createApiClient({
759
+ baseURL: 'https://api.example.com',
760
+ authOptions: {
761
+ refreshUrl: '/auth/refresh',
762
+
763
+ // Called after successful token refresh
764
+ onTokenRefreshed: (response) => {
765
+ const { user, permissions } = response.data
766
+
767
+ // Update app state
768
+ store.commit('SET_USER', user)
769
+ store.commit('SET_PERMISSIONS', permissions)
770
+ },
771
+
772
+ onTokenRefreshFailed: () => {
773
+ localStorage.clear()
774
+ window.location.href = '/login'
775
+ }
892
776
  }
893
777
  })
894
778
  ```
895
779
 
780
+ ---
781
+
896
782
  ## ๐Ÿ“š API Reference
897
783
 
898
784
  ### `useApi<T, D>(url, options)`
@@ -983,11 +869,14 @@ The main composable for making HTTP requests.
983
869
  data: Ref<T | null> // Response data
984
870
  loading: Ref<boolean> // Loading state
985
871
  error: Ref<ApiError | null> // Error object
872
+ statusCode: Ref<number | null> // HTTP status code
986
873
  response: Ref<AxiosResponse<T>> // Full Axios response
987
874
 
988
875
  // Methods
989
876
  execute: (config?: AxiosRequestConfig) => Promise<T | null>
877
+ setData: (data: T | null | ((prev: T | null) => T | null)) => void
990
878
  abort: (reason?: string) => void
879
+ reset: () => void
991
880
  }
992
881
  ```
993
882
 
@@ -1004,6 +893,24 @@ await execute()
1004
893
  await execute({ params: { page: 2 } })
1005
894
  ```
1006
895
 
896
+ #### `setData(newData)`
897
+ Manually update the `data` ref. Supports direct values or updater functions:
898
+
899
+ ```typescript
900
+ const { data, setData } = useApi<User[]>('/users')
901
+
902
+ // Direct value
903
+ setData([{ id: 1, name: 'John' }])
904
+
905
+ // Updater function (like React's setState)
906
+ setData(prev => prev ? [...prev, newUser] : [newUser])
907
+
908
+ // Remove item
909
+ setData(prev => prev?.filter(u => u.id !== userId) ?? null)
910
+ ```
911
+
912
+ > **Note:** `setData` automatically clears any existing error.
913
+
1007
914
  #### `abort(reason?)`
1008
915
  Cancel the current request:
1009
916
 
@@ -1016,6 +923,20 @@ execute()
1016
923
  setTimeout(() => abort('Timeout'), 5000)
1017
924
  ```
1018
925
 
926
+ #### `reset()`
927
+ Reset all state to initial values:
928
+
929
+ ```typescript
930
+ const { data, error, loading, reset } = useApi('/users')
931
+
932
+ // Clear everything
933
+ reset()
934
+ // data.value = null, error.value = null, loading.value = false
935
+
936
+ // Cancel after 5 seconds
937
+ setTimeout(() => abort('Timeout'), 5000)
938
+ ```
939
+
1019
940
  ---
1020
941
 
1021
942
  ### `createApiClient(options)`