@ametie/vue-muza-use 0.4.9 → 0.4.10

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.
Files changed (2) hide show
  1. package/README.md +320 -551
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -40,114 +40,10 @@ yarn add @ametie/vue-muza-use axios
40
40
 
41
41
  ---
42
42
 
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
43
  ## 🚀 Quick Start
150
44
 
45
+ Get started in 2 minutes with minimal configuration.
46
+
151
47
  ### 1. Setup Plugin (`main.ts`)
152
48
 
153
49
  ```typescript
@@ -157,30 +53,20 @@ import App from './App.vue'
157
53
 
158
54
  const app = createApp(App)
159
55
 
160
- // Create configured Axios instance
56
+ // Create API client with minimal config
161
57
  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
- }
58
+ baseURL: 'https://api.example.com'
170
59
  })
171
60
 
172
61
  // Install plugin
173
- app.use(createApi({
174
- axios: api,
175
- onError: (error) => {
176
- console.error('API Error:', error.message)
177
- }
178
- }))
62
+ app.use(createApi({ axios: api }))
179
63
 
180
64
  app.mount('#app')
181
65
  ```
182
66
 
183
- ### 2. Simple GET Request
67
+ > 💡 **That's it!** No auth configuration needed to get started. Add it later when you need it.
68
+
69
+ ### 2. Your First Request
184
70
 
185
71
  ```vue
186
72
  <script setup lang="ts">
@@ -193,7 +79,7 @@ interface User {
193
79
  }
194
80
 
195
81
  const { data, loading, error } = useApi<User>('/users/1', {
196
- immediate: true
82
+ immediate: true // Auto-fetch on mount
197
83
  })
198
84
  </script>
199
85
 
@@ -207,29 +93,23 @@ const { data, loading, error } = useApi<User>('/users/1', {
207
93
  </template>
208
94
  ```
209
95
 
210
- ### 3. Real-World Example: Live Search
96
+ ### 3. Live Search with Debounce
211
97
 
212
- This is where the library shines. Use a **getter function** for dynamic URLs, watch a ref, debounce input, and handle race conditions automatically:
98
+ This example shows the library's power: **automatic race condition handling** and **debouncing** built-in.
213
99
 
214
100
  ```vue
215
101
  <script setup lang="ts">
216
102
  import { ref } from 'vue'
217
103
  import { useApi } from '@ametie/vue-muza-use'
218
104
 
219
- interface Product {
220
- id: number
221
- name: string
222
- price: number
223
- }
224
-
225
105
  const searchQuery = ref('')
226
106
 
227
- // 💡 Pass a getter function - no need for computed!
228
- const { data, loading } = useApi<Product[]>(
107
+ // 💡 Use a getter function for dynamic URLs
108
+ const { data, loading } = useApi(
229
109
  () => `/products/search?q=${searchQuery.value}`,
230
110
  {
231
- watch: searchQuery, // ⚡️ Auto-refetch when query changes
232
- debounce: 500 // ⏱️ Wait 500ms after typing stops
111
+ watch: searchQuery, // Auto-refetch when query changes
112
+ debounce: 500 // Wait 500ms after typing stops
233
113
  }
234
114
  )
235
115
  </script>
@@ -247,274 +127,145 @@ const { data, loading } = useApi<Product[]>(
247
127
  </template>
248
128
  ```
249
129
 
130
+ **🎯 What just happened?**
131
+ - No race conditions — previous searches auto-cancel
132
+ - Debounce — waits for user to stop typing
133
+ - TypeScript — full type safety
134
+ - Clean code — no manual cleanup needed
135
+
250
136
  ---
251
137
 
252
- ## 📖 Core Usage
138
+ ## 📖 Basic Usage
253
139
 
254
140
  ### GET Requests
255
141
 
256
- #### Basic Fetch
142
+ #### Manual Execution
257
143
  ```typescript
258
144
  const { data, loading, error, execute } = useApi<User>('/users/1')
259
145
 
260
- // Manually trigger
146
+ // Trigger manually (e.g., on button click)
261
147
  await execute()
262
148
  ```
263
149
 
264
150
  #### Auto-Fetch on Mount
265
151
  ```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
152
+ const { data } = useApi<User>('/users/1', {
153
+ immediate: true // Fetches automatically
270
154
  })
271
155
  ```
272
156
 
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
283
- })
284
-
285
- // Also works: computed ref
286
- const url = computed(() => `/users/${userId.value}`)
287
- const { data } = useApi(url, { watch: userId, immediate: true })
288
- ```
289
-
290
- #### Query Parameters
291
- Pass `params` as static object or reactive ref:
292
-
157
+ #### With Query Parameters
293
158
  ```typescript
294
159
  const filters = ref({
295
160
  status: 'active',
296
- sort: 'name',
297
161
  limit: 20
298
162
  })
299
163
 
300
164
  const { data } = useApi('/users', {
301
- params: filters, // Automatically unwrapped
302
- watch: filters, // Re-fetch when filters change
303
- debounce: 300
165
+ params: filters, // Automatically unwrapped
166
+ watch: filters, // Re-fetch when filters change
167
+ immediate: true
304
168
  })
305
-
306
- // URL becomes: /users?status=active&sort=name&limit=20
307
169
  ```
308
170
 
309
171
  ---
310
172
 
311
173
  ### POST/PUT/PATCH Requests
312
174
 
313
- #### Form Submission with Loading State
175
+ #### Simple Form Submission
314
176
  ```vue
315
177
  <script setup lang="ts">
316
178
  import { ref } from 'vue'
317
179
  import { useApi } from '@ametie/vue-muza-use'
318
180
 
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>({
181
+ const form = ref({
330
182
  email: '',
331
183
  password: ''
332
184
  })
333
185
 
334
- const { execute, loading, error } = useApi<LoginResponse>('/auth/login', {
186
+ const { execute, loading, error } = useApi('/auth/login', {
335
187
  method: 'POST',
336
- authMode: 'public', // 👈 Skip token injection for login
337
- data: form, // Ref is auto-unwrapped
188
+ data: form, // Ref is auto-unwrapped
338
189
  onSuccess: (response) => {
339
- localStorage.setItem('token', response.data.token)
340
- router.push('/dashboard')
190
+ console.log('Logged in!', response.data)
341
191
  }
342
192
  })
343
-
344
- const handleSubmit = async () => {
345
- await execute()
346
- }
347
193
  </script>
348
194
 
349
195
  <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">
196
+ <form @submit.prevent="execute">
197
+ <input v-model="form.email" type="email" />
198
+ <input v-model="form.password" type="password" />
199
+ <button :disabled="loading">
355
200
  {{ loading ? 'Signing in...' : 'Sign In' }}
356
201
  </button>
357
-
358
- <p v-if="error" class="error">{{ error.message }}</p>
202
+ <p v-if="error">{{ error.message }}</p>
359
203
  </form>
360
204
  </template>
361
205
  ```
362
206
 
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
207
  ---
382
208
 
383
- ### Auto-Refetching with Watch
209
+ ## 🎯 Core Features
384
210
 
385
- Watch reactive dependencies and auto-refetch when they change. Perfect for filters, search, and dynamic content.
211
+ ### Watch & Auto-Refetch
386
212
 
387
- #### Live Search (Debounced)
388
- ```vue
389
- <script setup lang="ts">
213
+ Watch refs and automatically refetch when they change. Perfect for filters, search, and dynamic content.
214
+
215
+ #### Single Dependency
216
+ ```typescript
217
+ const userId = ref(1)
218
+
219
+ const { data } = useApi(() => `/users/${userId.value}`, {
220
+ watch: userId,
221
+ immediate: true
222
+ })
223
+
224
+ // Change userId → automatic refetch
225
+ userId.value = 2
226
+ ```
227
+
228
+ #### Multiple Dependencies
229
+ ```typescript
390
230
  const searchQuery = ref('')
391
231
  const category = ref('all')
392
232
 
393
- // Use getter function for dynamic URL construction
394
- const { data, loading } = useApi<Product[]>(
233
+ const { data } = useApi(
395
234
  () => `/products?q=${searchQuery.value}&category=${category.value}`,
396
235
  {
397
- watch: [searchQuery, category], // Watch multiple refs
398
- debounce: 500 // Debounce search input
236
+ watch: [searchQuery, category],
237
+ debounce: 500
399
238
  }
400
239
  )
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
240
  ```
475
241
 
476
242
  #### Auto-Save Form
477
243
  ```typescript
478
244
  const settings = ref({
479
245
  theme: 'dark',
480
- notifications: true,
481
- language: 'en'
246
+ notifications: true
482
247
  })
483
248
 
484
249
  useApi('/user/settings', {
485
250
  method: 'PUT',
486
251
  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
- }
252
+ watch: settings, // Deep watch by default
253
+ debounce: 1000, // Save 1s after changes stop
254
+ onSuccess: () => toast.success('Saved!')
492
255
  })
493
256
  ```
494
257
 
495
258
  ---
496
259
 
497
- ### Auto-Polling (Background Updates)
260
+ ### Polling (Background Updates)
498
261
 
499
- Keep data fresh with intelligent polling. Automatically pauses when tab is hidden (configurable).
262
+ Keep data fresh with smart polling. Automatically pauses when browser tab is hidden.
500
263
 
501
264
  #### Simple Polling
502
265
  ```typescript
503
- // Fetch notifications every 5 seconds
504
- const { data } = useApi<Notification[]>('/notifications', {
266
+ const { data } = useApi('/notifications', {
505
267
  immediate: true,
506
- poll: 5000
507
- })
508
- ```
509
-
510
- #### Smart Polling with Controls
511
- ```typescript
512
- const { data, loading } = useApi('/stock-prices', {
513
- immediate: true,
514
- poll: {
515
- interval: 1000,
516
- whenHidden: false // ⚠️ Pause when tab is hidden (default)
517
- }
268
+ poll: 5000 // Fetch every 5 seconds
518
269
  })
519
270
  ```
520
271
 
@@ -522,71 +273,162 @@ const { data, loading } = useApi('/stock-prices', {
522
273
  ```typescript
523
274
  const pollInterval = ref(3000)
524
275
 
525
- // Start/stop polling by changing the ref
526
276
  const { data } = useApi('/live-feed', {
527
277
  poll: pollInterval,
528
278
  immediate: true
529
279
  })
530
280
 
531
281
  // Stop polling
532
- const stopPolling = () => pollInterval.value = 0
282
+ pollInterval.value = 0
533
283
 
534
- // Resume with 5s interval
535
- const startPolling = () => pollInterval.value = 5000
284
+ // Resume with different interval
285
+ pollInterval.value = 5000
536
286
  ```
537
287
 
538
288
  ---
539
289
 
540
- ### Race Condition Prevention
290
+ ### Error Handling
541
291
 
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:
292
+ #### Per-Request Error Handling
293
+ ```typescript
294
+ const { error, execute } = useApi('/users', {
295
+ onError: (error) => {
296
+ if (error.status === 404) {
297
+ toast.error('User not found')
298
+ } else {
299
+ toast.error('Something went wrong')
300
+ }
301
+ },
302
+ skipErrorNotification: true // Skip global handler
303
+ })
304
+ ```
544
305
 
545
- ```vue
546
- <script setup lang="ts">
547
- import { useAbortController } from '@ametie/vue-muza-use'
306
+ #### Retry on Failure
307
+ ```typescript
308
+ useApi('/flaky-endpoint', {
309
+ immediate: true,
310
+ retry: 3, // Retry 3 times
311
+ retryDelay: 1000 // Wait 1s between retries
312
+ })
313
+ ```
314
+
315
+ ---
548
316
 
549
- const filters = ref({ category: 'all', priceMin: 0, priceMax: 1000 })
317
+ ### Loading States
550
318
 
551
- // 💡 useGlobalAbort: true by default - automatically subscribed
552
- const { data: products } = useApi('/products', {
553
- params: filters
554
- })
319
+ #### Per-Request Loading
320
+ ```typescript
321
+ const { data: user, loading: userLoading } = useApi('/user')
322
+ const { data: posts, loading: postsLoading } = useApi('/posts')
555
323
 
556
- const { data: stats } = useApi('/products/stats', {
557
- params: filters
324
+ // Each request tracks its own loading state
325
+ ```
326
+
327
+ #### Lifecycle Hooks
328
+ ```typescript
329
+ const { execute } = useApi('/analytics', {
330
+ onBefore: () => {
331
+ loadingBar.start()
332
+ },
333
+ onSuccess: (response) => {
334
+ console.log('Success!', response.data)
335
+ },
336
+ onError: (error) => {
337
+ console.error('Failed:', error.message)
338
+ },
339
+ onFinish: () => {
340
+ loadingBar.finish() // Always called
341
+ }
558
342
  })
343
+ ```
559
344
 
560
- const { abortAll } = useAbortController()
345
+ ---
561
346
 
562
- const resetFilters = () => {
563
- abortAll() // 🛑 Cancel both pending requests
564
- filters.value = { category: 'all', priceMin: 0, priceMax: 1000 }
565
- }
347
+ ## 📊 Real-World Examples
348
+
349
+ ### Data Table with Pagination & Sorting
350
+ ```vue
351
+ <script setup lang="ts">
352
+ const page = ref(1)
353
+ const sortBy = ref('created_at')
354
+ const sortOrder = ref<'asc' | 'desc'>('desc')
355
+
356
+ const params = computed(() => ({
357
+ page: page.value,
358
+ sort_by: sortBy.value,
359
+ sort_order: sortOrder.value,
360
+ per_page: 20
361
+ }))
566
362
 
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()
363
+ const { data, loading } = useApi('/orders', {
364
+ params,
365
+ watch: params,
366
+ immediate: true
570
367
  })
571
368
  </script>
369
+
370
+ <template>
371
+ <table>
372
+ <thead>
373
+ <tr>
374
+ <th @click="sortBy = 'id'">ID</th>
375
+ <th @click="sortBy = 'created_at'">Date</th>
376
+ <th @click="sortBy = 'total'">Total</th>
377
+ </tr>
378
+ </thead>
379
+ <tbody v-if="!loading">
380
+ <tr v-for="order in data?.data" :key="order.id">
381
+ <td>{{ order.id }}</td>
382
+ <td>{{ order.created_at }}</td>
383
+ <td>${{ order.total }}</td>
384
+ </tr>
385
+ </tbody>
386
+ </table>
387
+
388
+ <Pagination v-model="page" :total="data?.total" />
389
+ </template>
572
390
  ```
573
391
 
574
- #### Per-Request Abort
392
+ ### Optimistic UI Updates
575
393
  ```typescript
576
- const { execute, abort } = useApi('/long-running-task')
394
+ const { execute: updateProfile } = useApi('/user/profile', {
395
+ method: 'PUT',
396
+ onBefore: () => {
397
+ // Show update immediately
398
+ localProfile.value = { ...localProfile.value, ...changes }
399
+ },
400
+ onError: () => {
401
+ // Rollback on error
402
+ localProfile.value = originalProfile
403
+ toast.error('Update failed')
404
+ }
405
+ })
406
+ ```
577
407
 
578
- // Start
579
- execute()
408
+ ### Request Cancellation
409
+ ```typescript
410
+ import { useAbortController } from '@ametie/vue-muza-use'
411
+
412
+ const { abortAll } = useAbortController()
413
+
414
+ // Multiple requests
415
+ const { data: products } = useApi('/products', { params: filters })
416
+ const { data: stats } = useApi('/stats', { params: filters })
580
417
 
581
- // Cancel if needed
582
- setTimeout(() => abort('User cancelled'), 5000)
418
+ // Cancel all when filters reset
419
+ const resetFilters = () => {
420
+ abortAll() // 🛑 Cancel both requests
421
+ filters.value = { /* defaults */ }
422
+ }
583
423
  ```
584
424
 
425
+ ---
426
+
585
427
  ## ⚙️ Advanced Configuration
586
428
 
587
429
  ### Custom Axios Instance
588
430
 
589
- If you need full control over Axios configuration, create your own instance:
431
+ Full control over Axios configuration:
590
432
 
591
433
  ```typescript
592
434
  import axios from 'axios'
@@ -595,14 +437,12 @@ import { createApi } from '@ametie/vue-muza-use'
595
437
  const customAxios = axios.create({
596
438
  baseURL: 'https://api.example.com',
597
439
  timeout: 30000,
598
- headers: {
599
- 'X-Custom-Header': 'value'
600
- }
440
+ headers: { 'X-Custom-Header': 'value' }
601
441
  })
602
442
 
603
- // Add your own interceptors
443
+ // Add custom interceptors
604
444
  customAxios.interceptors.request.use((config) => {
605
- // Custom logic
445
+ // Your logic
606
446
  return config
607
447
  })
608
448
 
@@ -611,42 +451,36 @@ app.use(createApi({ axios: customAxios }))
611
451
 
612
452
  ---
613
453
 
614
- ### Error Handling
615
-
616
- #### Custom Error Parser
454
+ ### Global Error Handler
617
455
 
618
- Every backend returns errors differently. Normalize them into a standard format:
456
+ Normalize errors from different backend formats:
619
457
 
620
458
  ```typescript
621
- import { createApi } from '@ametie/vue-muza-use'
622
-
623
459
  app.use(createApi({
624
460
  axios: api,
461
+
462
+ // Global error handler
463
+ onError: (error) => {
464
+ toast.error(error.message)
465
+ },
466
+
467
+ // Error parser (normalize backend responses)
625
468
  errorParser: (error: any) => {
626
469
  const response = error.response?.data
627
470
 
628
- // Laravel/Rails validation errors
471
+ // Laravel validation errors
629
472
  if (response?.errors) {
630
473
  return {
631
474
  message: 'Validation Failed',
632
475
  status: error.response.status,
633
476
  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
477
+ errors: response.errors
644
478
  }
645
479
  }
646
480
 
647
- // Default fallback
481
+ // Default format
648
482
  return {
649
- message: error.message || 'An unexpected error occurred',
483
+ message: response?.message || error.message || 'Unknown error',
650
484
  status: error.response?.status || 500,
651
485
  details: error
652
486
  }
@@ -654,245 +488,180 @@ app.use(createApi({
654
488
  }))
655
489
  ```
656
490
 
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
491
  ---
689
492
 
690
- ### Authentication & Token Management
493
+ ## 🔐 Authentication & Token Management
691
494
 
692
- #### How Token Refresh Works
495
+ > **Note:** Authentication setup is optional. Only add this if your API requires JWT tokens.
693
496
 
694
- The library implements a **request queue pattern** to handle token expiration gracefully:
497
+ ### Basic Auth Setup
695
498
 
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
- ```
706
-
707
- This happens **transparently** — your components just wait a bit longer for the response.
708
-
709
- #### Configuration
499
+ Add authentication to your API client:
710
500
 
711
501
  ```typescript
712
502
  const api = createApiClient({
713
503
  baseURL: 'https://api.example.com',
714
- withAuth: true,
504
+ withAuth: true, // Enable automatic token injection
715
505
  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
-
506
+ refreshUrl: '/auth/refresh',
731
507
  onTokenRefreshFailed: () => {
732
- // Called when refresh fails (expired refresh token)
733
- localStorage.clear()
508
+ // Redirect to login when refresh fails
734
509
  window.location.href = '/login'
735
510
  }
736
511
  }
737
512
  })
738
513
  ```
739
514
 
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
- }
515
+ The library automatically:
516
+ - Injects `Authorization: Bearer <token>` header
517
+ - Refreshes expired tokens
518
+ - Queues requests during token refresh
519
+ - Retries failed requests after refresh
753
520
 
754
- export const clearTokens = (): void => {
755
- localStorage.removeItem('access_token')
756
- }
757
- ```
521
+ ---
758
522
 
759
- The `createApiClient` automatically injects `Authorization: Bearer <token>` on every request.
523
+ ### Token Management Modes
760
524
 
761
- #### Public Endpoints
525
+ #### Mode 1: localStorage (Default)
762
526
 
763
- For endpoints that don't require authentication (login, register, public data):
527
+ Simple setup for development or internal tools:
764
528
 
765
529
  ```typescript
766
- useApi('/auth/login', {
767
- method: 'POST',
768
- authMode: 'public', // 👈 Skips Authorization header
769
- data: credentials
530
+ const api = createApiClient({
531
+ baseURL: 'https://api.example.com',
532
+ authOptions: {
533
+ refreshUrl: '/auth/refresh',
534
+ onTokenRefreshFailed: () => router.push('/login')
535
+ }
770
536
  })
771
537
  ```
772
538
 
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
- ```
539
+ **Storage:** Both `accessToken` and `refreshToken` in localStorage
540
+ **Security:** ⚠️ Vulnerable to XSS attacks
541
+ **Use case:** Development, internal tools
787
542
 
788
543
  ---
789
544
 
790
- ### Lifecycle Hooks
545
+ #### Mode 2: httpOnly Cookies (Production)
791
546
 
792
- Fine-grained control over request lifecycle:
547
+ Recommended for production apps with sensitive data:
793
548
 
794
549
  ```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()
550
+ const api = createApiClient({
551
+ baseURL: 'https://api.example.com',
552
+ authOptions: {
553
+ refreshUrl: '/auth/refresh',
554
+ refreshWithCredentials: true, // 🔑 Send cookies for refresh
555
+ onTokenRefreshFailed: () => router.push('/login')
811
556
  }
812
557
  })
813
558
  ```
814
559
 
815
- **Hook Execution Order:**
816
- 1. `onBefore`
817
- 2. Request execution
818
- 3. `onSuccess` OR `onError`
819
- 4. `onFinish` (always runs)
560
+ **Storage:** Only `accessToken` in localStorage, `refreshToken` in httpOnly cookie
561
+ **Security:** 🔒 Protected from XSS attacks
562
+ **Backend requirement:** Must set `Set-Cookie` with `HttpOnly; Secure; SameSite`
820
563
 
821
- ---
564
+ **Common Issues:**
565
+ - **Cookie not sent?** Check cookie domain and `SameSite` attribute
566
+ - **CORS error?** Backend must set `Access-Control-Allow-Credentials: true`
567
+ - **401 on refresh?** Verify cookie is included in request headers
822
568
 
823
- ### Retry Logic
569
+ ---
824
570
 
825
- Intelligent retries with lifecycle awareness — retries stop if component unmounts:
571
+ ### Saving Tokens After Login
826
572
 
827
573
  ```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`)
574
+ import { tokenManager } from '@ametie/vue-muza-use'
575
+
576
+ const { execute } = useApi('/auth/login', {
577
+ method: 'POST',
578
+ authMode: 'public', // No auth for login endpoint
579
+ onSuccess(response) {
580
+ tokenManager.setTokens({
581
+ accessToken: response.data.accessToken,
582
+ refreshToken: response.data.refreshToken, // Optional in cookie mode
583
+ expiresIn: response.data.expiresIn
584
+ })
585
+
586
+ router.push('/dashboard')
833
587
  }
834
588
  })
835
589
  ```
836
590
 
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
591
  ---
847
592
 
848
- ### Global vs Local Loading States
593
+ ### Public Endpoints
594
+
595
+ Skip authentication for public endpoints:
849
596
 
850
- #### Local Loading (Per Request)
851
597
  ```typescript
852
- const { data: user, loading: userLoading } = useApi('/user')
853
- const { data: posts, loading: postsLoading } = useApi('/posts')
598
+ // Login (no auth needed)
599
+ useApi('/auth/login', {
600
+ method: 'POST',
601
+ authMode: 'public',
602
+ data: credentials
603
+ })
854
604
 
855
- // Each request has its own loading state
605
+ // Public blog posts
606
+ useApi('/blog/posts', {
607
+ authMode: 'public',
608
+ immediate: true
609
+ })
856
610
  ```
857
611
 
858
- #### Global Loading (Shared)
859
- ```typescript
860
- const globalLoading = ref(false)
612
+ ---
861
613
 
862
- useApi('/user', {
863
- onBefore: () => globalLoading.value = true,
864
- onFinish: () => globalLoading.value = false
865
- })
614
+ ### Advanced: Custom Refresh Payload
615
+
616
+ Send additional data with token refresh requests:
866
617
 
867
- useApi('/posts', {
868
- onBefore: () => globalLoading.value = true,
869
- onFinish: () => globalLoading.value = false
618
+ ```typescript
619
+ const api = createApiClient({
620
+ baseURL: 'https://api.example.com',
621
+ authOptions: {
622
+ refreshUrl: '/auth/refresh',
623
+
624
+ // ⚠️ Use function for dynamic data
625
+ refreshPayload: () => ({
626
+ refreshToken: tokenManager.getRefreshToken(),
627
+ deviceId: getDeviceId(),
628
+ timestamp: Date.now()
629
+ })
630
+ }
870
631
  })
871
632
  ```
872
633
 
873
- #### Smart Loading (Debounced)
874
- Prevent loading flicker for fast responses:
634
+ ---
635
+
636
+ ### Advanced: Token Refresh Callback
637
+
638
+ Handle additional data from refresh response:
875
639
 
876
640
  ```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
641
+ const api = createApiClient({
642
+ baseURL: 'https://api.example.com',
643
+ authOptions: {
644
+ refreshUrl: '/auth/refresh',
645
+
646
+ // Called after successful token refresh
647
+ onTokenRefreshed: (response) => {
648
+ const { user, permissions } = response.data
649
+
650
+ // Update app state
651
+ store.commit('SET_USER', user)
652
+ store.commit('SET_PERMISSIONS', permissions)
653
+ },
654
+
655
+ onTokenRefreshFailed: () => {
656
+ localStorage.clear()
657
+ window.location.href = '/login'
658
+ }
892
659
  }
893
660
  })
894
661
  ```
895
662
 
663
+ ---
664
+
896
665
  ## 📚 API Reference
897
666
 
898
667
  ### `useApi<T, D>(url, options)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ametie/vue-muza-use",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "Powerful Vue 3 API composable (Muza Kit) with Axios, Auto-Refresh & TypeScript",
5
5
  "author": "MortyQ",
6
6
  "license": "MIT",