@ametie/vue-muza-use 0.4.8 → 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.
package/README.md CHANGED
@@ -40,96 +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`
75
-
76
- ### Custom Refresh Payload
77
- Send additional data with refresh requests:
78
-
79
- **⚠️ IMPORTANT:** Use functions for dynamic data (like tokens from storage):
80
-
81
- ```typescript
82
- const api = createApiClient({
83
- baseURL: 'https://api.example.com',
84
- authOptions: {
85
- refreshUrl: '/auth/refresh',
86
- // ❌ WRONG - computed once at app start
87
- // refreshPayload: { refreshToken: tokenManager.getRefreshToken() }
88
-
89
- // ✅ CORRECT - computed on each refresh
90
- refreshPayload: () => ({
91
- refreshToken: tokenManager.getRefreshToken(),
92
- timestamp: Date.now()
93
- })
94
- }
95
- })
96
- ```
97
-
98
- Static payload (for constants only):
99
- ```typescript
100
- refreshPayload: {
101
- deviceId: 'mobile-v1',
102
- platform: 'ios' // constants that don't change
103
- }
104
- ```
105
-
106
- Dynamic function:
107
- ```typescript
108
- refreshPayload: () => ({
109
- timestamp: Date.now(),
110
- sessionId: getSessionId() // reads current value
111
- })
112
- ```
113
-
114
- ### Saving Tokens After Login
115
- ```typescript
116
- import { tokenManager } from '@ametie/vue-muza-use'
117
-
118
- const { execute } = useApiPost<AuthResponse>('/auth/login', {
119
- authMode: 'public',
120
- onSuccess(response) {
121
- tokenManager.setTokens({
122
- accessToken: response.data.accessToken,
123
- refreshToken: response.data.refreshToken, // optional in cookie mode
124
- expiresIn: response.data.expiresIn
125
- })
126
- }
127
- })
128
- ```
129
- ---
130
-
131
43
  ## 🚀 Quick Start
132
44
 
45
+ Get started in 2 minutes with minimal configuration.
46
+
133
47
  ### 1. Setup Plugin (`main.ts`)
134
48
 
135
49
  ```typescript
@@ -139,30 +53,20 @@ import App from './App.vue'
139
53
 
140
54
  const app = createApp(App)
141
55
 
142
- // Create configured Axios instance
56
+ // Create API client with minimal config
143
57
  const api = createApiClient({
144
- baseURL: 'https://api.example.com',
145
- withAuth: true, // Automatic token injection
146
- authOptions: {
147
- refreshUrl: '/auth/refresh',
148
- onTokenRefreshFailed: () => {
149
- window.location.href = '/login'
150
- }
151
- }
58
+ baseURL: 'https://api.example.com'
152
59
  })
153
60
 
154
61
  // Install plugin
155
- app.use(createApi({
156
- axios: api,
157
- onError: (error) => {
158
- console.error('API Error:', error.message)
159
- }
160
- }))
62
+ app.use(createApi({ axios: api }))
161
63
 
162
64
  app.mount('#app')
163
65
  ```
164
66
 
165
- ### 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
166
70
 
167
71
  ```vue
168
72
  <script setup lang="ts">
@@ -175,7 +79,7 @@ interface User {
175
79
  }
176
80
 
177
81
  const { data, loading, error } = useApi<User>('/users/1', {
178
- immediate: true
82
+ immediate: true // Auto-fetch on mount
179
83
  })
180
84
  </script>
181
85
 
@@ -189,29 +93,23 @@ const { data, loading, error } = useApi<User>('/users/1', {
189
93
  </template>
190
94
  ```
191
95
 
192
- ### 3. Real-World Example: Live Search
96
+ ### 3. Live Search with Debounce
193
97
 
194
- 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.
195
99
 
196
100
  ```vue
197
101
  <script setup lang="ts">
198
102
  import { ref } from 'vue'
199
103
  import { useApi } from '@ametie/vue-muza-use'
200
104
 
201
- interface Product {
202
- id: number
203
- name: string
204
- price: number
205
- }
206
-
207
105
  const searchQuery = ref('')
208
106
 
209
- // 💡 Pass a getter function - no need for computed!
210
- const { data, loading } = useApi<Product[]>(
107
+ // 💡 Use a getter function for dynamic URLs
108
+ const { data, loading } = useApi(
211
109
  () => `/products/search?q=${searchQuery.value}`,
212
110
  {
213
- watch: searchQuery, // ⚡️ Auto-refetch when query changes
214
- debounce: 500 // ⏱️ Wait 500ms after typing stops
111
+ watch: searchQuery, // Auto-refetch when query changes
112
+ debounce: 500 // Wait 500ms after typing stops
215
113
  }
216
114
  )
217
115
  </script>
@@ -229,274 +127,145 @@ const { data, loading } = useApi<Product[]>(
229
127
  </template>
230
128
  ```
231
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
+
232
136
  ---
233
137
 
234
- ## 📖 Core Usage
138
+ ## 📖 Basic Usage
235
139
 
236
140
  ### GET Requests
237
141
 
238
- #### Basic Fetch
142
+ #### Manual Execution
239
143
  ```typescript
240
144
  const { data, loading, error, execute } = useApi<User>('/users/1')
241
145
 
242
- // Manually trigger
146
+ // Trigger manually (e.g., on button click)
243
147
  await execute()
244
148
  ```
245
149
 
246
150
  #### Auto-Fetch on Mount
247
151
  ```typescript
248
- useApi<User>('/users/1', {
249
- immediate: true,
250
- retry: 3, // Retry 3 times on network failure
251
- retryDelay: 1000 // Wait 1s between retries
252
- })
253
- ```
254
-
255
- #### Dynamic URLs
256
- Use a **getter function** for reactive URLs - simple and efficient:
257
-
258
- ```typescript
259
- const userId = ref(1)
260
-
261
- // ✅ Preferred: Getter function (no computed needed!)
262
- const { data } = useApi(() => `/users/${userId.value}`, {
263
- watch: userId,
264
- immediate: true
152
+ const { data } = useApi<User>('/users/1', {
153
+ immediate: true // Fetches automatically
265
154
  })
266
-
267
- // Also works: computed ref
268
- const url = computed(() => `/users/${userId.value}`)
269
- const { data } = useApi(url, { watch: userId, immediate: true })
270
155
  ```
271
156
 
272
- #### Query Parameters
273
- Pass `params` as static object or reactive ref:
274
-
157
+ #### With Query Parameters
275
158
  ```typescript
276
159
  const filters = ref({
277
160
  status: 'active',
278
- sort: 'name',
279
161
  limit: 20
280
162
  })
281
163
 
282
164
  const { data } = useApi('/users', {
283
- params: filters, // Automatically unwrapped
284
- watch: filters, // Re-fetch when filters change
285
- debounce: 300
165
+ params: filters, // Automatically unwrapped
166
+ watch: filters, // Re-fetch when filters change
167
+ immediate: true
286
168
  })
287
-
288
- // URL becomes: /users?status=active&sort=name&limit=20
289
169
  ```
290
170
 
291
171
  ---
292
172
 
293
173
  ### POST/PUT/PATCH Requests
294
174
 
295
- #### Form Submission with Loading State
175
+ #### Simple Form Submission
296
176
  ```vue
297
177
  <script setup lang="ts">
298
178
  import { ref } from 'vue'
299
179
  import { useApi } from '@ametie/vue-muza-use'
300
180
 
301
- interface LoginForm {
302
- email: string
303
- password: string
304
- }
305
-
306
- interface LoginResponse {
307
- token: string
308
- user: { id: number; name: string }
309
- }
310
-
311
- const form = ref<LoginForm>({
181
+ const form = ref({
312
182
  email: '',
313
183
  password: ''
314
184
  })
315
185
 
316
- const { execute, loading, error } = useApi<LoginResponse>('/auth/login', {
186
+ const { execute, loading, error } = useApi('/auth/login', {
317
187
  method: 'POST',
318
- authMode: 'public', // 👈 Skip token injection for login
319
- data: form, // Ref is auto-unwrapped
188
+ data: form, // Ref is auto-unwrapped
320
189
  onSuccess: (response) => {
321
- localStorage.setItem('token', response.data.token)
322
- router.push('/dashboard')
190
+ console.log('Logged in!', response.data)
323
191
  }
324
192
  })
325
-
326
- const handleSubmit = async () => {
327
- await execute()
328
- }
329
193
  </script>
330
194
 
331
195
  <template>
332
- <form @submit.prevent="handleSubmit">
333
- <input v-model="form.email" type="email" :disabled="loading" />
334
- <input v-model="form.password" type="password" :disabled="loading" />
335
-
336
- <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">
337
200
  {{ loading ? 'Signing in...' : 'Sign In' }}
338
201
  </button>
339
-
340
- <p v-if="error" class="error">{{ error.message }}</p>
202
+ <p v-if="error">{{ error.message }}</p>
341
203
  </form>
342
204
  </template>
343
205
  ```
344
206
 
345
- #### Update with Optimistic UI
346
- ```typescript
347
- const { execute: updateProfile } = useApi('/user/profile', {
348
- method: 'PUT',
349
- onBefore: () => {
350
- // Show optimistic update
351
- localProfile.value = { ...localProfile.value, ...changes }
352
- },
353
- onSuccess: (response) => {
354
- toast.success('Profile updated!')
355
- },
356
- onError: () => {
357
- // Rollback on error
358
- localProfile.value = originalProfile
359
- }
360
- })
361
- ```
362
-
363
207
  ---
364
208
 
365
- ### Auto-Refetching with Watch
209
+ ## 🎯 Core Features
366
210
 
367
- Watch reactive dependencies and auto-refetch when they change. Perfect for filters, search, and dynamic content.
211
+ ### Watch & Auto-Refetch
368
212
 
369
- #### Live Search (Debounced)
370
- ```vue
371
- <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
372
230
  const searchQuery = ref('')
373
231
  const category = ref('all')
374
232
 
375
- // Use getter function for dynamic URL construction
376
- const { data, loading } = useApi<Product[]>(
233
+ const { data } = useApi(
377
234
  () => `/products?q=${searchQuery.value}&category=${category.value}`,
378
235
  {
379
- watch: [searchQuery, category], // Watch multiple refs
380
- debounce: 500 // Debounce search input
236
+ watch: [searchQuery, category],
237
+ debounce: 500
381
238
  }
382
239
  )
383
- </script>
384
-
385
- <template>
386
- <input v-model="searchQuery" placeholder="Search..." />
387
- <select v-model="category">
388
- <option value="all">All</option>
389
- <option value="electronics">Electronics</option>
390
- </select>
391
-
392
- <ProductList :products="data" :loading="loading" />
393
- </template>
394
- ```
395
-
396
- #### Data Table with Pagination & Sorting
397
- ```vue
398
- <script setup lang="ts">
399
- interface PaginatedResponse<T> {
400
- data: T[]
401
- total: number
402
- page: number
403
- }
404
-
405
- const page = ref(1)
406
- const sortBy = ref('created_at')
407
- const sortOrder = ref<'asc' | 'desc'>('desc')
408
-
409
- const params = computed(() => ({
410
- page: page.value,
411
- sort_by: sortBy.value,
412
- sort_order: sortOrder.value,
413
- per_page: 20
414
- }))
415
-
416
- const { data, loading } = useApi<PaginatedResponse<Order>>('/orders', {
417
- params,
418
- watch: params, // Auto-refetch when any param changes
419
- immediate: true
420
- })
421
-
422
- const handleSort = (column: string) => {
423
- if (sortBy.value === column) {
424
- sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
425
- } else {
426
- sortBy.value = column
427
- sortOrder.value = 'desc'
428
- }
429
- }
430
- </script>
431
-
432
- <template>
433
- <table>
434
- <thead>
435
- <tr>
436
- <th @click="handleSort('id')">ID</th>
437
- <th @click="handleSort('created_at')">Date</th>
438
- <th @click="handleSort('total')">Total</th>
439
- </tr>
440
- </thead>
441
- <tbody v-if="!loading">
442
- <tr v-for="order in data?.data" :key="order.id">
443
- <td>{{ order.id }}</td>
444
- <td>{{ order.created_at }}</td>
445
- <td>${{ order.total }}</td>
446
- </tr>
447
- </tbody>
448
- </table>
449
-
450
- <Pagination
451
- v-model="page"
452
- :total="data?.total"
453
- :loading="loading"
454
- />
455
- </template>
456
240
  ```
457
241
 
458
242
  #### Auto-Save Form
459
243
  ```typescript
460
244
  const settings = ref({
461
245
  theme: 'dark',
462
- notifications: true,
463
- language: 'en'
246
+ notifications: true
464
247
  })
465
248
 
466
249
  useApi('/user/settings', {
467
250
  method: 'PUT',
468
251
  data: settings,
469
- watch: settings, // Deep watch by default
470
- debounce: 1000, // Save 1s after changes stop
471
- onSuccess: () => {
472
- toast.success('Settings saved')
473
- }
252
+ watch: settings, // Deep watch by default
253
+ debounce: 1000, // Save 1s after changes stop
254
+ onSuccess: () => toast.success('Saved!')
474
255
  })
475
256
  ```
476
257
 
477
258
  ---
478
259
 
479
- ### Auto-Polling (Background Updates)
260
+ ### Polling (Background Updates)
480
261
 
481
- 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.
482
263
 
483
264
  #### Simple Polling
484
265
  ```typescript
485
- // Fetch notifications every 5 seconds
486
- const { data } = useApi<Notification[]>('/notifications', {
266
+ const { data } = useApi('/notifications', {
487
267
  immediate: true,
488
- poll: 5000
489
- })
490
- ```
491
-
492
- #### Smart Polling with Controls
493
- ```typescript
494
- const { data, loading } = useApi('/stock-prices', {
495
- immediate: true,
496
- poll: {
497
- interval: 1000,
498
- whenHidden: false // ⚠️ Pause when tab is hidden (default)
499
- }
268
+ poll: 5000 // Fetch every 5 seconds
500
269
  })
501
270
  ```
502
271
 
@@ -504,71 +273,162 @@ const { data, loading } = useApi('/stock-prices', {
504
273
  ```typescript
505
274
  const pollInterval = ref(3000)
506
275
 
507
- // Start/stop polling by changing the ref
508
276
  const { data } = useApi('/live-feed', {
509
277
  poll: pollInterval,
510
278
  immediate: true
511
279
  })
512
280
 
513
281
  // Stop polling
514
- const stopPolling = () => pollInterval.value = 0
282
+ pollInterval.value = 0
515
283
 
516
- // Resume with 5s interval
517
- const startPolling = () => pollInterval.value = 5000
284
+ // Resume with different interval
285
+ pollInterval.value = 5000
518
286
  ```
519
287
 
520
288
  ---
521
289
 
522
- ### Race Condition Prevention
290
+ ### Error Handling
291
+
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
+ ```
523
305
 
524
- #### Global Abort Controller
525
- Cancel all pending requests in a scope when filters change. By default, all requests subscribe to the global abort controller:
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
+ ```
526
314
 
527
- ```vue
528
- <script setup lang="ts">
529
- import { useAbortController } from '@ametie/vue-muza-use'
315
+ ---
530
316
 
531
- const filters = ref({ category: 'all', priceMin: 0, priceMax: 1000 })
317
+ ### Loading States
532
318
 
533
- // 💡 useGlobalAbort: true by default - automatically subscribed
534
- const { data: products } = useApi('/products', {
535
- params: filters
536
- })
319
+ #### Per-Request Loading
320
+ ```typescript
321
+ const { data: user, loading: userLoading } = useApi('/user')
322
+ const { data: posts, loading: postsLoading } = useApi('/posts')
323
+
324
+ // Each request tracks its own loading state
325
+ ```
537
326
 
538
- const { data: stats } = useApi('/products/stats', {
539
- params: filters
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
+ }
540
342
  })
343
+ ```
541
344
 
542
- const { abortAll } = useAbortController()
345
+ ---
543
346
 
544
- const resetFilters = () => {
545
- abortAll() // 🛑 Cancel both pending requests
546
- filters.value = { category: 'all', priceMin: 0, priceMax: 1000 }
547
- }
347
+ ## 📊 Real-World Examples
548
348
 
549
- // To opt-out of global abort:
550
- const { data: independent } = useApi('/independent-request', {
551
- useGlobalAbort: false // This request won't be cancelled by abortAll()
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
+ }))
362
+
363
+ const { data, loading } = useApi('/orders', {
364
+ params,
365
+ watch: params,
366
+ immediate: true
552
367
  })
553
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>
390
+ ```
391
+
392
+ ### Optimistic UI Updates
393
+ ```typescript
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
+ })
554
406
  ```
555
407
 
556
- #### Per-Request Abort
408
+ ### Request Cancellation
557
409
  ```typescript
558
- const { execute, abort } = useApi('/long-running-task')
410
+ import { useAbortController } from '@ametie/vue-muza-use'
559
411
 
560
- // Start
561
- execute()
412
+ const { abortAll } = useAbortController()
562
413
 
563
- // Cancel if needed
564
- setTimeout(() => abort('User cancelled'), 5000)
414
+ // Multiple requests
415
+ const { data: products } = useApi('/products', { params: filters })
416
+ const { data: stats } = useApi('/stats', { params: filters })
417
+
418
+ // Cancel all when filters reset
419
+ const resetFilters = () => {
420
+ abortAll() // 🛑 Cancel both requests
421
+ filters.value = { /* defaults */ }
422
+ }
565
423
  ```
566
424
 
425
+ ---
426
+
567
427
  ## ⚙️ Advanced Configuration
568
428
 
569
429
  ### Custom Axios Instance
570
430
 
571
- If you need full control over Axios configuration, create your own instance:
431
+ Full control over Axios configuration:
572
432
 
573
433
  ```typescript
574
434
  import axios from 'axios'
@@ -577,14 +437,12 @@ import { createApi } from '@ametie/vue-muza-use'
577
437
  const customAxios = axios.create({
578
438
  baseURL: 'https://api.example.com',
579
439
  timeout: 30000,
580
- headers: {
581
- 'X-Custom-Header': 'value'
582
- }
440
+ headers: { 'X-Custom-Header': 'value' }
583
441
  })
584
442
 
585
- // Add your own interceptors
443
+ // Add custom interceptors
586
444
  customAxios.interceptors.request.use((config) => {
587
- // Custom logic
445
+ // Your logic
588
446
  return config
589
447
  })
590
448
 
@@ -593,42 +451,36 @@ app.use(createApi({ axios: customAxios }))
593
451
 
594
452
  ---
595
453
 
596
- ### Error Handling
454
+ ### Global Error Handler
597
455
 
598
- #### Custom Error Parser
599
-
600
- Every backend returns errors differently. Normalize them into a standard format:
456
+ Normalize errors from different backend formats:
601
457
 
602
458
  ```typescript
603
- import { createApi } from '@ametie/vue-muza-use'
604
-
605
459
  app.use(createApi({
606
460
  axios: api,
461
+
462
+ // Global error handler
463
+ onError: (error) => {
464
+ toast.error(error.message)
465
+ },
466
+
467
+ // Error parser (normalize backend responses)
607
468
  errorParser: (error: any) => {
608
469
  const response = error.response?.data
609
470
 
610
- // Laravel/Rails validation errors
471
+ // Laravel validation errors
611
472
  if (response?.errors) {
612
473
  return {
613
474
  message: 'Validation Failed',
614
475
  status: error.response.status,
615
476
  code: 'VALIDATION_ERROR',
616
- errors: response.errors // { email: ['Invalid email format'] }
617
- }
618
- }
619
-
620
- // Custom API format
621
- if (response?.error?.message) {
622
- return {
623
- message: response.error.message,
624
- status: error.response?.status || 500,
625
- code: response.error.code
477
+ errors: response.errors
626
478
  }
627
479
  }
628
480
 
629
- // Default fallback
481
+ // Default format
630
482
  return {
631
- message: error.message || 'An unexpected error occurred',
483
+ message: response?.message || error.message || 'Unknown error',
632
484
  status: error.response?.status || 500,
633
485
  details: error
634
486
  }
@@ -636,245 +488,180 @@ app.use(createApi({
636
488
  }))
637
489
  ```
638
490
 
639
- #### Using Errors in Components
640
-
641
- ```vue
642
- <script setup lang="ts">
643
- const { data, error, execute } = useApi('/users', {
644
- skipErrorNotification: true // Skip global error handler
645
- })
646
-
647
- // Access structured error
648
- if (error.value) {
649
- console.log(error.value.message) // User-friendly message
650
- console.log(error.value.status) // HTTP status code
651
- console.log(error.value.code) // Custom error code
652
- console.log(error.value.errors) // Validation errors
653
- }
654
- </script>
655
-
656
- <template>
657
- <div v-if="error" class="error-banner">
658
- {{ error.message }}
659
-
660
- <!-- Display validation errors -->
661
- <ul v-if="error.errors">
662
- <li v-for="(msgs, field) in error.errors" :key="field">
663
- <strong>{{ field }}:</strong> {{ msgs.join(', ') }}
664
- </li>
665
- </ul>
666
- </div>
667
- </template>
668
- ```
669
-
670
491
  ---
671
492
 
672
- ### Authentication & Token Management
493
+ ## 🔐 Authentication & Token Management
673
494
 
674
- #### How Token Refresh Works
495
+ > **Note:** Authentication setup is optional. Only add this if your API requires JWT tokens.
675
496
 
676
- The library implements a **request queue pattern** to handle token expiration gracefully:
497
+ ### Basic Auth Setup
677
498
 
678
- ```
679
- 1. Request fails with 401 Unauthorized
680
-
681
- 2. Add request to queue & pause all new requests
682
-
683
- 3. Attempt token refresh (POST to refreshUrl)
684
-
685
- 4. Success? → Replay all queued requests with new token
686
- Failure? → Call onTokenRefreshFailed() & reject queue
687
- ```
688
-
689
- This happens **transparently** — your components just wait a bit longer for the response.
690
-
691
- #### Configuration
499
+ Add authentication to your API client:
692
500
 
693
501
  ```typescript
694
502
  const api = createApiClient({
695
503
  baseURL: 'https://api.example.com',
696
- withAuth: true,
504
+ withAuth: true, // Enable automatic token injection
697
505
  authOptions: {
698
- refreshUrl: '/auth/refresh', // Default: '/auth/refresh'
699
-
700
- // ✨ NEW: Handle successful refresh response
701
- onTokenRefreshed: (response) => {
702
- // Extract additional data from refresh response
703
- const { user, permissions } = response.data
704
-
705
- // Update app state
706
- store.commit('SET_USER', user)
707
- store.commit('SET_PERMISSIONS', permissions)
708
-
709
- // Analytics
710
- analytics.track('token_refreshed', { userId: user.id })
711
- },
712
-
506
+ refreshUrl: '/auth/refresh',
713
507
  onTokenRefreshFailed: () => {
714
- // Called when refresh fails (expired refresh token)
715
- localStorage.clear()
508
+ // Redirect to login when refresh fails
716
509
  window.location.href = '/login'
717
510
  }
718
511
  }
719
512
  })
720
513
  ```
721
514
 
722
- #### Token Storage
723
-
724
- The library expects you to manage token storage. Implement these utilities:
725
-
726
- ```typescript
727
- // utils/auth.ts
728
- export const getAccessToken = (): string | null => {
729
- return localStorage.getItem('access_token')
730
- }
731
-
732
- export const setAccessToken = (token: string): void => {
733
- localStorage.setItem('access_token', token)
734
- }
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
735
520
 
736
- export const clearTokens = (): void => {
737
- localStorage.removeItem('access_token')
738
- }
739
- ```
521
+ ---
740
522
 
741
- The `createApiClient` automatically injects `Authorization: Bearer <token>` on every request.
523
+ ### Token Management Modes
742
524
 
743
- #### Public Endpoints
525
+ #### Mode 1: localStorage (Default)
744
526
 
745
- For endpoints that don't require authentication (login, register, public data):
527
+ Simple setup for development or internal tools:
746
528
 
747
529
  ```typescript
748
- useApi('/auth/login', {
749
- method: 'POST',
750
- authMode: 'public', // 👈 Skips Authorization header
751
- data: credentials
530
+ const api = createApiClient({
531
+ baseURL: 'https://api.example.com',
532
+ authOptions: {
533
+ refreshUrl: '/auth/refresh',
534
+ onTokenRefreshFailed: () => router.push('/login')
535
+ }
752
536
  })
753
537
  ```
754
538
 
755
- #### Monitoring Auth Events
756
-
757
- Track authentication events for debugging or analytics:
758
-
759
- ```typescript
760
- import { setAuthMonitor } from '@ametie/vue-muza-use'
761
-
762
- setAuthMonitor((event, payload) => {
763
- console.log(`[Auth Event] ${event}`, payload)
764
-
765
- // Events: 'token_refresh_start', 'token_refresh_success',
766
- // 'token_refresh_failed', 'token_injected'
767
- })
768
- ```
539
+ **Storage:** Both `accessToken` and `refreshToken` in localStorage
540
+ **Security:** ⚠️ Vulnerable to XSS attacks
541
+ **Use case:** Development, internal tools
769
542
 
770
543
  ---
771
544
 
772
- ### Lifecycle Hooks
545
+ #### Mode 2: httpOnly Cookies (Production)
773
546
 
774
- Fine-grained control over request lifecycle:
547
+ Recommended for production apps with sensitive data:
775
548
 
776
549
  ```typescript
777
- const { execute } = useApi('/analytics', {
778
- onBefore: () => {
779
- console.log('Request starting...')
780
- loadingBar.start()
781
- },
782
- onSuccess: (response) => {
783
- console.log('Success!', response.data)
784
- analytics.track('api_success', { endpoint: '/analytics' })
785
- },
786
- onError: (error) => {
787
- console.error('Failed:', error.message)
788
- sentry.captureException(error)
789
- },
790
- onFinish: () => {
791
- console.log('Request finished (success or error)')
792
- 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')
793
556
  }
794
557
  })
795
558
  ```
796
559
 
797
- **Hook Execution Order:**
798
- 1. `onBefore`
799
- 2. Request execution
800
- 3. `onSuccess` OR `onError`
801
- 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`
802
563
 
803
- ---
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
804
568
 
805
- ### Retry Logic
569
+ ---
806
570
 
807
- Intelligent retries with lifecycle awareness — retries stop if component unmounts:
571
+ ### Saving Tokens After Login
808
572
 
809
573
  ```typescript
810
- useApi('/flaky-endpoint', {
811
- retry: 3, // Retry up to 3 times
812
- retryDelay: 1000, // Wait 1s between attempts
813
- onError: (error, attempt) => {
814
- 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')
815
587
  }
816
588
  })
817
589
  ```
818
590
 
819
- **When retries happen:**
820
- - Network errors (timeouts, connection refused)
821
- - 5xx server errors
822
- - Configurable via axios retry interceptor
823
-
824
- **When retries DON'T happen:**
825
- - 4xx client errors (bad request, unauthorized, etc.)
826
- - Component is unmounted (automatic cleanup)
827
-
828
591
  ---
829
592
 
830
- ### Global vs Local Loading States
593
+ ### Public Endpoints
594
+
595
+ Skip authentication for public endpoints:
831
596
 
832
- #### Local Loading (Per Request)
833
597
  ```typescript
834
- const { data: user, loading: userLoading } = useApi('/user')
835
- 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
+ })
836
604
 
837
- // Each request has its own loading state
605
+ // Public blog posts
606
+ useApi('/blog/posts', {
607
+ authMode: 'public',
608
+ immediate: true
609
+ })
838
610
  ```
839
611
 
840
- #### Global Loading (Shared)
841
- ```typescript
842
- const globalLoading = ref(false)
612
+ ---
843
613
 
844
- useApi('/user', {
845
- onBefore: () => globalLoading.value = true,
846
- onFinish: () => globalLoading.value = false
847
- })
614
+ ### Advanced: Custom Refresh Payload
615
+
616
+ Send additional data with token refresh requests:
848
617
 
849
- useApi('/posts', {
850
- onBefore: () => globalLoading.value = true,
851
- 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
+ }
852
631
  })
853
632
  ```
854
633
 
855
- #### Smart Loading (Debounced)
856
- Prevent loading flicker for fast responses:
634
+ ---
635
+
636
+ ### Advanced: Token Refresh Callback
637
+
638
+ Handle additional data from refresh response:
857
639
 
858
640
  ```typescript
859
- import { ref, watch } from 'vue'
860
-
861
- const loading = ref(false)
862
- const showLoading = ref(false)
863
- let timeout: ReturnType<typeof setTimeout>
864
-
865
- watch(loading, (val) => {
866
- if (val) {
867
- // Show loading after 200ms (skip if request is fast)
868
- timeout = setTimeout(() => {
869
- showLoading.value = true
870
- }, 200)
871
- } else {
872
- clearTimeout(timeout)
873
- 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
+ }
874
659
  }
875
660
  })
876
661
  ```
877
662
 
663
+ ---
664
+
878
665
  ## 📚 API Reference
879
666
 
880
667
  ### `useApi<T, D>(url, options)`
package/dist/index.cjs CHANGED
@@ -51,7 +51,9 @@ module.exports = __toCommonJS(index_exports);
51
51
  // src/plugin.ts
52
52
  var import_vue = require("vue");
53
53
  var API_INJECTION_KEY = /* @__PURE__ */ Symbol("use-api-config");
54
+ var globalConfig = null;
54
55
  function createApi(options) {
56
+ globalConfig = options;
55
57
  return {
56
58
  install(app) {
57
59
  app.provide(API_INJECTION_KEY, options);
@@ -59,9 +61,14 @@ function createApi(options) {
59
61
  };
60
62
  }
61
63
  function useApiConfig() {
62
- const config = (0, import_vue.inject)(API_INJECTION_KEY);
64
+ if (globalConfig) {
65
+ return globalConfig;
66
+ }
67
+ const config = (0, import_vue.inject)(API_INJECTION_KEY, null);
63
68
  if (!config) {
64
- throw new Error("API plugin not installed! Did you forget app.use(createApi(...))?");
69
+ throw new Error(
70
+ "API plugin not installed!\n\nMake sure you have called app.use(createApi(...)) in your main.ts:\n\nimport { createApi, createApiClient } from '@ametie/vue-muza-use'\n\nconst api = createApiClient({ baseURL: '...' })\n\napp.use(createApi({\n axios: api,\n onError: (error) => console.error(error)\n}))"
71
+ );
65
72
  }
66
73
  return config;
67
74
  }
@@ -599,12 +606,13 @@ function setupInterceptors(axiosInstance, options = {}) {
599
606
  if (refreshPayload) {
600
607
  payload = typeof refreshPayload === "function" ? await refreshPayload() : refreshPayload;
601
608
  }
609
+ const shouldUseCredentials = refreshWithCredentials || !tokenManager.getRefreshToken();
602
610
  const response = await axiosInstance.post(
603
611
  refreshUrl,
604
612
  payload,
605
613
  {
606
614
  authMode: "public",
607
- withCredentials: refreshWithCredentials
615
+ withCredentials: shouldUseCredentials
608
616
  }
609
617
  );
610
618
  let accessToken;
@@ -618,7 +626,9 @@ function setupInterceptors(axiosInstance, options = {}) {
618
626
  accessToken = data.accessToken || data.access_token;
619
627
  refreshToken = data.refreshToken || data.refresh_token;
620
628
  }
621
- if (!accessToken) throw new Error("No access token in refresh response");
629
+ if (!accessToken) {
630
+ throw new Error("No access token in refresh response");
631
+ }
622
632
  tokenManager.setTokens({ accessToken, refreshToken });
623
633
  if (onTokenRefreshed) {
624
634
  await onTokenRefreshed(response);
package/dist/index.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/plugin.ts
2
2
  import { inject } from "vue";
3
3
  var API_INJECTION_KEY = /* @__PURE__ */ Symbol("use-api-config");
4
+ var globalConfig = null;
4
5
  function createApi(options) {
6
+ globalConfig = options;
5
7
  return {
6
8
  install(app) {
7
9
  app.provide(API_INJECTION_KEY, options);
@@ -9,9 +11,14 @@ function createApi(options) {
9
11
  };
10
12
  }
11
13
  function useApiConfig() {
12
- const config = inject(API_INJECTION_KEY);
14
+ if (globalConfig) {
15
+ return globalConfig;
16
+ }
17
+ const config = inject(API_INJECTION_KEY, null);
13
18
  if (!config) {
14
- throw new Error("API plugin not installed! Did you forget app.use(createApi(...))?");
19
+ throw new Error(
20
+ "API plugin not installed!\n\nMake sure you have called app.use(createApi(...)) in your main.ts:\n\nimport { createApi, createApiClient } from '@ametie/vue-muza-use'\n\nconst api = createApiClient({ baseURL: '...' })\n\napp.use(createApi({\n axios: api,\n onError: (error) => console.error(error)\n}))"
21
+ );
15
22
  }
16
23
  return config;
17
24
  }
@@ -549,12 +556,13 @@ function setupInterceptors(axiosInstance, options = {}) {
549
556
  if (refreshPayload) {
550
557
  payload = typeof refreshPayload === "function" ? await refreshPayload() : refreshPayload;
551
558
  }
559
+ const shouldUseCredentials = refreshWithCredentials || !tokenManager.getRefreshToken();
552
560
  const response = await axiosInstance.post(
553
561
  refreshUrl,
554
562
  payload,
555
563
  {
556
564
  authMode: "public",
557
- withCredentials: refreshWithCredentials
565
+ withCredentials: shouldUseCredentials
558
566
  }
559
567
  );
560
568
  let accessToken;
@@ -568,7 +576,9 @@ function setupInterceptors(axiosInstance, options = {}) {
568
576
  accessToken = data.accessToken || data.access_token;
569
577
  refreshToken = data.refreshToken || data.refresh_token;
570
578
  }
571
- if (!accessToken) throw new Error("No access token in refresh response");
579
+ if (!accessToken) {
580
+ throw new Error("No access token in refresh response");
581
+ }
572
582
  tokenManager.setTokens({ accessToken, refreshToken });
573
583
  if (onTokenRefreshed) {
574
584
  await onTokenRefreshed(response);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ametie/vue-muza-use",
3
- "version": "0.4.8",
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",