vue-api-kit 1.10.8 → 1.10.9

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 (3) hide show
  1. package/README.md +223 -723
  2. package/dist/index.js +14 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,4 @@
1
1
 
2
-
3
2
  # 🚀 vue-api-kit
4
3
 
5
4
  [![NPM Version](https://img.shields.io/npm/v/vue-api-kit.svg?style=flat-square)](https://www.npmjs.com/package/vue-api-kit)
@@ -11,6 +10,24 @@
11
10
 
12
11
  A powerful and type-safe API client for Vue 3 applications with built-in validation using Zod.
13
12
 
13
+ ## 📋 Table of Contents
14
+
15
+ - [Installation](#-installation)
16
+ - [Quick Start](#-quick-start)
17
+ - [Core Features](#-core-features)
18
+ - [Basic Usage](#-basic-usage)
19
+ - [Queries (GET)](#queries-get)
20
+ - [Queries (POST)](#queries-post)
21
+ - [Mutations (POST/PUT/DELETE)](#mutations-postputdelete)
22
+ - [Configuration](#-configuration)
23
+ - [Advanced Features](#-advanced-features)
24
+ - [Nested Structure](#nested-structure)
25
+ - [Modular API Definitions](#modular-api-definitions)
26
+ - [Request Interceptors](#request-interceptors)
27
+ - [File Upload](#file-upload)
28
+ - [CSRF Protection](#csrf-protection)
29
+ - [License](#-license)
30
+
14
31
  ## 📦 Installation
15
32
 
16
33
  ```bash
@@ -25,7 +42,7 @@ import { z } from 'zod';
25
42
 
26
43
  // Define your API client
27
44
  const api = createApiClient({
28
- baseURL: 'https://jsonplaceholder.typicode.com',
45
+ baseURL: 'https://api.example.com',
29
46
  queries: {
30
47
  getUsers: {
31
48
  path: '/users',
@@ -43,23 +60,6 @@ const api = createApiClient({
43
60
  name: z.string(),
44
61
  email: z.string()
45
62
  })
46
- },
47
- // POST query for complex searches
48
- searchUsers: {
49
- method: 'POST',
50
- path: '/users/search',
51
- data: z.object({
52
- query: z.string(),
53
- filters: z.object({
54
- active: z.boolean().optional(),
55
- role: z.string().optional()
56
- }).optional()
57
- }),
58
- response: z.array(z.object({
59
- id: z.number(),
60
- name: z.string(),
61
- email: z.string()
62
- }))
63
63
  }
64
64
  },
65
65
  mutations: {
@@ -75,141 +75,149 @@ const api = createApiClient({
75
75
  name: z.string(),
76
76
  email: z.string()
77
77
  })
78
- },
79
- updateUser: {
80
- method: 'PUT',
81
- path: '/users/{id}',
82
- params: z.object({ id: z.number() }),
83
- data: z.object({
84
- name: z.string(),
85
- email: z.string().email()
86
- })
87
- },
88
- deleteUser: {
89
- method: 'DELETE',
90
- path: '/users/{id}',
91
- params: z.object({ id: z.number() })
92
78
  }
93
79
  }
94
80
  });
95
81
  ```
96
82
 
97
- ## 📖 Usage in Vue Components
83
+ Use in your Vue components:
84
+
85
+ ```vue
86
+ <script setup lang="ts">
87
+ import { api } from './api';
88
+
89
+ // Query - auto-loads on mount
90
+ const { result, isLoading, errorMessage } = api.query.getUsers();
91
+
92
+ // Mutation
93
+ const { mutate, isLoading: creating } = api.mutation.createUser();
94
+
95
+ async function handleCreate() {
96
+ await mutate({ name: 'John', email: 'john@example.com' });
97
+ }
98
+ </script>
99
+
100
+ <template>
101
+ <div v-if="isLoading">Loading...</div>
102
+ <div v-else-if="errorMessage">Error: {{ errorMessage }}</div>
103
+ <ul v-else>
104
+ <li v-for="user in result" :key="user.id">{{ user.name }}</li>
105
+ </ul>
106
+ </template>
107
+ ```
108
+
109
+ ## 🎯 Core Features
110
+
111
+ - ✅ **Type-Safe** - Full TypeScript support with automatic type inference
112
+ - ✅ **Zod Validation** - Built-in request/response validation
113
+ - ✅ **Vue 3 Composition API** - Reactive state management
114
+ - ✅ **Lightweight** - ~7kB minified (2.2kB gzipped)
115
+ - ✅ **Auto Loading States** - Built-in loading, error, and success states
116
+ - ✅ **Path Parameters** - Automatic path parameter replacement (`/users/{id}`)
117
+ - ✅ **Debouncing** - Built-in request debouncing
118
+ - ✅ **POST Queries** - Support both GET and POST for data fetching
119
+ - ✅ **File Upload** - Multipart/form-data with nested objects
120
+ - ✅ **CSRF Protection** - Automatic token refresh (Laravel Sanctum compatible)
121
+ - ✅ **Modular** - Split API definitions across files
122
+ - ✅ **Nested Structure** - Organize endpoints hierarchically
123
+ - ✅ **Tree-Shakeable** - Only bundles what you use
98
124
 
99
- ### Queries (GET and POST requests)
125
+ ## 📖 Basic Usage
100
126
 
101
- Queries support both GET and POST methods, allowing you to fetch data with complex search criteria.
127
+ ### Queries (GET)
102
128
 
103
- #### GET Queries
129
+ Use queries to fetch data. They automatically load on component mount:
104
130
 
105
131
  ```vue
106
132
  <script setup lang="ts">
107
133
  import { api } from './api';
134
+ import { ref } from 'vue';
108
135
 
109
- // Simple query - loads automatically on mount
136
+ // Simple query - automatically loads data on mount
110
137
  const { result, isLoading, errorMessage } = api.query.getUsers();
111
138
 
112
- // Query with parameters
139
+ // Query with parameters - reactive to parameter changes
113
140
  const userId = ref(1);
114
- const { result: user, isLoading: loading, refetch } = api.query.getUser({
141
+ const { result: user, refetch } = api.query.getUser({
115
142
  params: { id: userId }
116
143
  });
117
144
 
118
- // Query with options
145
+ // Query with options - customize behavior
119
146
  const { result: data } = api.query.getUsers({
120
147
  loadOnMount: true,
121
148
  debounce: 300,
122
- onResult: (data) => {
123
- console.log('Data loaded:', data);
124
- },
125
- onError: (error) => {
126
- console.error('Error:', error);
127
- }
149
+ onResult: (data) => console.log('Loaded:', data),
150
+ onError: (error) => console.error('Error:', error)
128
151
  });
129
152
  </script>
130
153
 
131
154
  <template>
132
- <div>
133
- <div v-if="isLoading">Loading...</div>
134
- <div v-else-if="errorMessage">Error: {{ errorMessage }}</div>
135
- <ul v-else>
136
- <li v-for="user in result" :key="user.id">
137
- {{ user.name }}
138
- </li>
139
- </ul>
140
- </div>
155
+ <div v-if="isLoading">Loading...</div>
156
+ <div v-else-if="errorMessage">Error: {{ errorMessage }}</div>
157
+ <ul v-else>
158
+ <li v-for="user in result" :key="user.id">{{ user.name }}</li>
159
+ </ul>
141
160
  </template>
142
161
  ```
143
162
 
144
- #### POST Queries
163
+ ### Queries (POST)
164
+
165
+ POST queries are perfect for complex searches with filters:
145
166
 
146
- POST queries are perfect for complex searches, filtering, or any operation that requires sending data in the request body.
167
+ ```typescript
168
+ // API definition
169
+ queries: {
170
+ searchUsers: {
171
+ method: 'POST',
172
+ path: '/users/search',
173
+ data: z.object({
174
+ query: z.string(),
175
+ filters: z.object({
176
+ active: z.boolean().optional(),
177
+ role: z.string().optional()
178
+ }).optional()
179
+ }),
180
+ response: z.array(z.object({ id: z.number(), name: z.string() }))
181
+ }
182
+ }
183
+ ```
147
184
 
148
185
  ```vue
149
186
  <script setup lang="ts">
150
- import { api } from './api';
151
- import { ref } from 'vue';
152
-
153
187
  const searchTerm = ref('');
154
-
155
188
  const { result, isLoading, refetch } = api.query.searchUsers({
156
189
  data: {
157
190
  query: searchTerm.value,
158
- filters: {
159
- active: true,
160
- role: 'admin'
161
- }
191
+ filters: { active: true }
162
192
  },
163
- loadOnMount: false,
164
- onResult: (data) => {
165
- console.log('Search results:', data);
166
- }
193
+ loadOnMount: false
167
194
  });
168
-
169
- const handleSearch = () => {
170
- refetch();
171
- };
172
195
  </script>
173
196
 
174
197
  <template>
175
- <div>
176
- <input v-model="searchTerm" @keyup.enter="handleSearch" />
177
- <button @click="handleSearch" :disabled="isLoading">Search</button>
178
-
179
- <div v-if="isLoading">Searching...</div>
180
- <div v-else-if="result">
181
- <div v-for="user in result" :key="user.id">
182
- {{ user.name }}
183
- </div>
184
- </div>
198
+ <input v-model="searchTerm" @keyup.enter="refetch" />
199
+ <button @click="refetch" :disabled="isLoading">Search</button>
200
+ <div v-if="isLoading">Searching...</div>
201
+ <div v-else-if="result">
202
+ <div v-for="user in result" :key="user.id">{{ user.name }}</div>
185
203
  </div>
186
204
  </template>
187
205
  ```
188
206
 
189
- ### Mutations (POST, PUT, DELETE)
207
+ ### Mutations (POST/PUT/DELETE)
190
208
 
191
209
  ```vue
192
210
  <script setup lang="ts">
193
- import { api } from './api';
194
- import { ref } from 'vue';
195
-
196
211
  const { mutate, isLoading, result, errorMessage } = api.mutation.createUser({
197
- onResult: (data) => {
198
- console.log('User created:', data);
199
- },
200
- onError: (error) => {
201
- console.error('Error:', error);
202
- }
212
+ onResult: (data) => console.log('Created:', data),
213
+ onError: (error) => console.error('Error:', error)
203
214
  });
204
215
 
205
216
  const name = ref('');
206
217
  const email = ref('');
207
218
 
208
219
  async function handleSubmit() {
209
- await mutate({
210
- name: name.value,
211
- email: email.value
212
- });
220
+ await mutate({ name: name.value, email: email.value });
213
221
  }
214
222
  </script>
215
223
 
@@ -225,30 +233,48 @@ async function handleSubmit() {
225
233
  </template>
226
234
  ```
227
235
 
228
- ## 🎯 Features
236
+ ## ⚙️ Configuration
229
237
 
230
- - ✅ **Type-Safe**: Full TypeScript support with automatic type inference
231
- - **Zod Validation**: Built-in request/response validation
232
- - ✅ **Vue 3 Composition API**: Reactive state management
233
- - ✅ **Lightweight**: ~7kB minified (2.2kB gzipped) - optimized for production
234
- - **Auto Loading States**: Built-in loading, error, and success states
235
- - ✅ **POST Queries**: Support for both GET and POST methods in queries for complex data retrieval
236
- - **Modular APIs**: Merge queries and mutations from separate files with full type safety
237
- - **Multi-Level Nesting**: Organize queries and mutations in nested structures with full type safety
238
- - ✅ **File Upload**: Support for multipart/form-data with nested objects in mutations
239
- - ✅ **Path Parameters**: Automatic path parameter replacement
240
- - ✅ **Debouncing**: Built-in request debouncing
241
- - ✅ **CSRF Protection**: Automatic CSRF token refresh on 403/419 errors
242
- - ✅ **Global Error Handling**: Centralized error management
243
- - ✅ **Request Interceptors**: Modify requests before sending
244
- - ✅ **Fully Typed**: Complete type inference for params, data, and response
245
- - ✅ **Tree-Shakeable**: Only bundles what you use
238
+ ```typescript
239
+ const api = createApiClient({
240
+ baseURL: 'https://api.example.com',
241
+ headers: {
242
+ 'Authorization': 'Bearer token'
243
+ },
244
+ withCredentials: true, // Enable cookies
245
+ withXSRFToken: true, // Enable XSRF token handling
246
246
 
247
- ## 🏗️ Multi-Level Nested Structure
247
+ // CSRF token refresh endpoint
248
+ csrfRefreshEndpoint: '/sanctum/csrf-cookie',
248
249
 
249
- Organize your API endpoints in a hierarchical structure for better code organization and maintainability.
250
+ // Global handlers
251
+ onBeforeRequest: async (config) => {
252
+ // Modify requests globally
253
+ const token = localStorage.getItem('token');
254
+ config.headers.Authorization = `Bearer ${token}`;
255
+ return config;
256
+ },
257
+
258
+ onError: (error) => {
259
+ // Global error handler
260
+ console.error('API Error:', error.message);
261
+ },
262
+
263
+ onZodError: (issues) => {
264
+ // Handle validation errors
265
+ console.error('Validation errors:', issues);
266
+ },
267
+
268
+ queries: { /* ... */ },
269
+ mutations: { /* ... */ }
270
+ });
271
+ ```
272
+
273
+ ## 🔧 Advanced Features
274
+
275
+ ### Nested Structure
250
276
 
251
- ### Basic Nested Structure
277
+ Organize endpoints hierarchically for better code organization:
252
278
 
253
279
  ```typescript
254
280
  import { createApiClient, defineQuery, defineMutation } from 'vue-api-kit';
@@ -256,24 +282,16 @@ import { z } from 'zod';
256
282
 
257
283
  const api = createApiClient({
258
284
  baseURL: 'https://api.example.com',
259
-
260
285
  queries: {
261
- // Organize queries by resource
262
286
  users: {
263
287
  getAll: defineQuery({
264
288
  path: '/users',
265
- response: z.array(z.object({
266
- id: z.number(),
267
- name: z.string()
268
- }))
289
+ response: z.array(z.object({ id: z.number(), name: z.string() }))
269
290
  }),
270
291
  getById: defineQuery({
271
292
  path: '/users/{id}',
272
293
  params: z.object({ id: z.number() }),
273
- response: z.object({
274
- id: z.number(),
275
- name: z.string()
276
- })
294
+ response: z.object({ id: z.number(), name: z.string() })
277
295
  }),
278
296
  search: defineQuery({
279
297
  method: 'POST',
@@ -294,13 +312,12 @@ const api = createApiClient({
294
312
  })
295
313
  }
296
314
  },
297
-
298
315
  mutations: {
299
316
  users: {
300
317
  create: defineMutation({
301
318
  method: 'POST',
302
319
  path: '/users',
303
- data: z.object({ name: z.string(), email: z.string().email() }),
320
+ data: z.object({ name: z.string(), email: z.string() }),
304
321
  response: z.object({ id: z.number(), name: z.string() })
305
322
  }),
306
323
  update: defineMutation({
@@ -319,292 +336,31 @@ const api = createApiClient({
319
336
  }
320
337
  });
321
338
 
322
- // Usage in components:
323
- const { result, isLoading } = api.query.users.getAll();
324
- const { mutate } = api.mutation.users.create();
339
+ // Usage
340
+ api.query.users.getAll()
341
+ api.mutation.users.create()
325
342
  ```
326
343
 
327
- ### Deep Nesting
344
+ **Benefits:** Better organization, namespace separation, improved readability, scalability.
328
345
 
329
- You can nest as deeply as needed for complex API structures:
346
+ ### Modular API Definitions
330
347
 
331
- ```typescript
332
- const api = createApiClient({
333
- baseURL: 'https://api.example.com',
334
-
335
- queries: {
336
- api: {
337
- v1: {
338
- admin: {
339
- users: {
340
- list: defineQuery({ path: '/api/v1/admin/users' }),
341
- search: defineQuery({
342
- method: 'POST',
343
- path: '/api/v1/admin/users/search'
344
- })
345
- },
346
- reports: {
347
- daily: defineQuery({ path: '/api/v1/admin/reports/daily' }),
348
- monthly: defineQuery({ path: '/api/v1/admin/reports/monthly' })
349
- }
350
- },
351
- public: {
352
- posts: {
353
- list: defineQuery({ path: '/api/v1/public/posts' })
354
- }
355
- }
356
- }
357
- }
358
- }
359
- });
360
-
361
- // Access deeply nested endpoints:
362
- api.query.api.v1.admin.users.list()
363
- api.query.api.v1.admin.reports.daily()
364
- api.query.api.v1.public.posts.list()
365
- ```
366
-
367
- ### Mixed Flat and Nested Structure
368
-
369
- You can combine flat and nested structures as needed:
370
-
371
- ```typescript
372
- const api = createApiClient({
373
- baseURL: 'https://api.example.com',
374
-
375
- queries: {
376
- // Flat queries
377
- getStatus: defineQuery({ path: '/status' }),
378
- getHealth: defineQuery({ path: '/health' }),
379
-
380
- // Nested queries
381
- users: {
382
- getAll: defineQuery({ path: '/users' }),
383
- getById: defineQuery({ path: '/users/{id}' })
384
- },
385
- posts: {
386
- getAll: defineQuery({ path: '/posts' })
387
- }
388
- }
389
- });
390
-
391
- // Both flat and nested work together:
392
- api.query.getStatus() // Flat
393
- api.query.users.getAll() // Nested
394
- ```
395
-
396
- ### Benefits
397
-
398
- - **Better Organization**: Group related endpoints together
399
- - **Improved Readability**: Clear hierarchical structure reflects your API design
400
- - **Namespace Separation**: Prevent naming conflicts (e.g., `users.create` vs `posts.create`)
401
- - **Scalability**: Easy to add new endpoints without cluttering the root level
402
- - **Type Safety**: Full TypeScript inference throughout the nested structure
403
- - **Backward Compatible**: Works alongside existing flat structure
404
-
405
- ## 🔧 Advanced Configuration
406
-
407
- ```typescript
408
- const api = createApiClient({
409
- baseURL: 'https://api.example.com',
410
- headers: {
411
- 'Authorization': 'Bearer token'
412
- },
413
- withCredentials: true, // Enable cookies
414
- withXSRFToken: true, // Enable automatic XSRF token handling
415
-
416
- // CSRF Token Protection
417
- csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Auto-refresh CSRF token on 403/419 errors
418
-
419
- // Global handlers
420
- onBeforeRequest: async (config) => {
421
- // Modify request before sending
422
- const token = localStorage.getItem('token');
423
- config.headers.Authorization = `Bearer ${token}`;
424
- return config;
425
- },
426
-
427
- onStartRequest: async () => {
428
- // Called when request starts
429
- console.log('Request started');
430
- },
431
-
432
- onFinishRequest: async () => {
433
- // Called when request finishes (success or error)
434
- console.log('Request finished');
435
- },
436
-
437
- onError: (error) => {
438
- // Global error handler
439
- console.error('API Error:', error.message);
440
- },
441
-
442
- onZodError: (issues) => {
443
- // Handle validation errors
444
- console.error('Validation errors:', issues);
445
- },
446
-
447
- queries: { /* ... */ },
448
- mutations: { /* ... */ }
449
- });
450
- ```
451
-
452
- ## 🎯 Per-Query and Per-Mutation Request Interceptors
453
-
454
- In addition to global request interceptors, you can define `onBeforeRequest` hooks for individual queries and mutations. This is useful when you need to append specific headers or modify the request configuration for certain endpoints only.
455
-
456
- ### Query-Level onBeforeRequest
457
-
458
- You can define `onBeforeRequest` in two ways for queries:
459
-
460
- **1. In the query definition:**
348
+ Split your API definitions across multiple files:
461
349
 
350
+ **user-api.ts**
462
351
  ```typescript
463
- const api = createApiClient({
464
- baseURL: 'https://api.example.com',
465
- queries: {
466
- getUser: {
467
- path: '/users/{id}',
468
- params: z.object({ id: z.number() }),
469
- response: z.object({ id: z.number(), name: z.string() }),
470
- // Query-level interceptor
471
- onBeforeRequest: async (config) => {
472
- config.headers['X-Custom-Query-Header'] = 'special-value';
473
- return config;
474
- }
475
- }
476
- }
477
- });
478
- ```
479
-
480
- **2. In the query options when calling it:**
481
-
482
- ```typescript
483
- const { result, isLoading } = api.query.getUser({
484
- params: { id: 1 },
485
- // Runtime interceptor
486
- onBeforeRequest: async (config) => {
487
- const token = await getAuthToken();
488
- config.headers.Authorization = `Bearer ${token}`;
489
- return config;
490
- }
491
- });
492
- ```
493
-
494
- ### Mutation-Level onBeforeRequest
495
-
496
- Similarly, you can define `onBeforeRequest` for mutations:
497
-
498
- **1. In the mutation definition:**
499
-
500
- ```typescript
501
- const api = createApiClient({
502
- baseURL: 'https://api.example.com',
503
- mutations: {
504
- createUser: {
505
- method: 'POST',
506
- path: '/users',
507
- data: z.object({ name: z.string(), email: z.string() }),
508
- response: z.object({ id: z.number(), name: z.string() }),
509
- // Mutation-level interceptor
510
- onBeforeRequest: async (config) => {
511
- config.headers['X-Action'] = 'create-user';
512
- return config;
513
- }
514
- }
515
- }
516
- });
517
- ```
518
-
519
- **2. In the mutation options when calling it:**
520
-
521
- ```typescript
522
- const { mutate } = api.mutation.createUser({
523
- // Runtime interceptor
524
- onBeforeRequest: async (config) => {
525
- const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
526
- if (csrfToken) {
527
- config.headers['X-CSRF-Token'] = csrfToken;
528
- }
529
- return config;
530
- }
531
- });
532
-
533
- await mutate({ data: { name: 'John', email: 'john@example.com' } });
534
- ```
535
-
536
- ### Execution Order
537
-
538
- When multiple `onBeforeRequest` hooks are defined, they execute in the following order:
539
-
540
- 1. **Global interceptor** (defined in `createApiClient` options) - Applied via axios interceptor
541
- 2. **Query/Mutation definition interceptor** (defined in query/mutation object)
542
- 3. **Options interceptor** (defined when calling the query/mutation)
543
-
544
- Each hook can modify the config, and later hooks can see and override changes made by earlier hooks.
545
-
546
- ### Use Cases
547
-
548
- - **Authentication**: Add tokens for specific endpoints that require authentication
549
- - **Custom Headers**: Append API keys, correlation IDs, or feature flags for specific requests
550
- - **Request Transformation**: Modify request data or parameters before sending
551
- - **Conditional Logic**: Apply different configurations based on runtime conditions
552
- - **Debugging**: Add request IDs or trace headers for specific endpoints
553
-
554
- ### Example: Dynamic Authorization
555
-
556
- ```typescript
557
- const api = createApiClient({
558
- baseURL: 'https://api.example.com',
559
- queries: {
560
- getProtectedData: {
561
- path: '/protected/data',
562
- response: z.object({ data: z.string() }),
563
- onBeforeRequest: async (config) => {
564
- // This query always needs fresh token
565
- const token = await refreshAndGetToken();
566
- config.headers.Authorization = `Bearer ${token}`;
567
- return config;
568
- }
569
- },
570
- getPublicData: {
571
- path: '/public/data',
572
- response: z.object({ data: z.string() })
573
- // No onBeforeRequest needed for public endpoint
574
- }
575
- }
576
- });
577
- ```
578
-
579
- ## 🧩 Modular API Definitions
580
-
581
- For large applications, you can organize your API definitions into separate files and merge them together with full type safety.
582
-
583
- ### Step 1: Define API modules in separate files
584
-
585
- **user-api.ts** - User-related queries and mutations
586
- ```typescript
587
- import { z, defineQuery, defineMutation } from 'vue-api-kit';
352
+ import { defineQuery, defineMutation } from 'vue-api-kit';
353
+ import { z } from 'zod';
588
354
 
589
355
  export const userQueries = {
590
356
  getUsers: defineQuery({
591
- method: 'GET',
592
357
  path: '/users',
593
- response: z.array(z.object({
594
- id: z.number(),
595
- name: z.string(),
596
- email: z.string().email()
597
- }))
358
+ response: z.array(z.object({ id: z.number(), name: z.string() }))
598
359
  }),
599
360
  getUser: defineQuery({
600
- method: 'GET',
601
361
  path: '/users/{id}',
602
362
  params: z.object({ id: z.number() }),
603
- response: z.object({
604
- id: z.number(),
605
- name: z.string(),
606
- email: z.string().email()
607
- })
363
+ response: z.object({ id: z.number(), name: z.string() })
608
364
  })
609
365
  };
610
366
 
@@ -612,386 +368,130 @@ export const userMutations = {
612
368
  createUser: defineMutation({
613
369
  method: 'POST',
614
370
  path: '/users',
615
- data: z.object({
616
- name: z.string(),
617
- email: z.string().email()
618
- }),
619
- response: z.object({
620
- id: z.number(),
621
- name: z.string(),
622
- email: z.string().email()
623
- })
624
- }),
625
- updateUser: defineMutation({
626
- method: 'PUT',
627
- path: '/users/{id}',
628
- params: z.object({ id: z.number() }),
629
- data: z.object({
630
- name: z.string().optional(),
631
- email: z.string().email().optional()
632
- }),
633
- response: z.object({
634
- id: z.number(),
635
- name: z.string(),
636
- email: z.string().email()
637
- })
638
- })
639
- };
640
- ```
641
-
642
- **post-api.ts** - Post-related queries and mutations
643
- ```typescript
644
- import { z, defineQuery, defineMutation } from 'vue-api-kit';
645
-
646
- export const postQueries = {
647
- getPosts: defineQuery({
648
- method: 'GET',
649
- path: '/posts',
650
- response: z.array(z.object({
651
- id: z.number(),
652
- title: z.string(),
653
- body: z.string()
654
- }))
655
- })
656
- };
657
-
658
- export const postMutations = {
659
- createPost: defineMutation({
660
- method: 'POST',
661
- path: '/posts',
662
- data: z.object({
663
- title: z.string(),
664
- body: z.string()
665
- }),
666
- response: z.object({
667
- id: z.number(),
668
- title: z.string(),
669
- body: z.string()
670
- })
371
+ data: z.object({ name: z.string(), email: z.string() }),
372
+ response: z.object({ id: z.number(), name: z.string() })
671
373
  })
672
374
  };
673
375
  ```
674
376
 
675
- ### Step 2: Merge API definitions
676
-
677
- **api.ts** - Main API client with merged definitions
377
+ **api.ts**
678
378
  ```typescript
679
379
  import { createApiClient, mergeQueries, mergeMutations } from 'vue-api-kit';
680
380
  import { userQueries, userMutations } from './user-api';
681
381
  import { postQueries, postMutations } from './post-api';
682
382
 
683
- // Approach 1: Merge queries and mutations separately
684
383
  export const api = createApiClient({
685
384
  baseURL: 'https://api.example.com',
686
-
687
- // Merge all queries from different modules
688
385
  queries: mergeQueries(userQueries, postQueries),
689
-
690
- // Merge all mutations from different modules
691
386
  mutations: mergeMutations(userMutations, postMutations)
692
387
  });
693
-
694
- // Now you can use all queries and mutations with full type safety!
695
- // api.query.getUsers() ✓ Fully typed
696
- // api.query.getPosts() ✓ Fully typed
697
- // api.mutation.createUser ✓ Fully typed
698
- // api.mutation.createPost ✓ Fully typed
699
388
  ```
700
389
 
701
- ### Nested Structure with Modular APIs
390
+ **Benefits:** Separation of concerns, reusability, team collaboration, full type safety.
702
391
 
703
- You can also use nested structures with modular API definitions:
392
+ ### Request Interceptors
704
393
 
705
- **user-api.ts** - User module with nested structure
706
- ```typescript
707
- import { z, defineQuery, defineMutation } from 'vue-api-kit';
708
-
709
- export const userApi = {
710
- queries: {
711
- users: {
712
- getAll: defineQuery({
713
- path: '/users',
714
- response: z.array(z.object({ id: z.number(), name: z.string() }))
715
- }),
716
- getById: defineQuery({
717
- path: '/users/{id}',
718
- params: z.object({ id: z.number() }),
719
- response: z.object({ id: z.number(), name: z.string() })
720
- })
721
- }
722
- },
723
- mutations: {
724
- users: {
725
- create: defineMutation({
726
- method: 'POST',
727
- path: '/users',
728
- data: z.object({ name: z.string() })
729
- }),
730
- update: defineMutation({
731
- method: 'PUT',
732
- path: '/users/{id}',
733
- params: z.object({ id: z.number() }),
734
- data: z.object({ name: z.string() })
735
- })
736
- }
737
- }
738
- };
739
- ```
740
-
741
- **post-api.ts** - Post module with nested structure
742
- ```typescript
743
- import { z, defineQuery, defineMutation } from 'vue-api-kit';
744
-
745
- export const postApi = {
746
- queries: {
747
- posts: {
748
- getAll: defineQuery({
749
- path: '/posts',
750
- response: z.array(z.object({ id: z.number(), title: z.string() }))
751
- }),
752
- getById: defineQuery({
753
- path: '/posts/{id}',
754
- params: z.object({ id: z.number() }),
755
- response: z.object({ id: z.number(), title: z.string() })
756
- })
757
- }
758
- },
759
- mutations: {
760
- posts: {
761
- create: defineMutation({
762
- method: 'POST',
763
- path: '/posts',
764
- data: z.object({ title: z.string(), content: z.string() })
765
- })
766
- }
767
- }
768
- };
769
- ```
394
+ Add interceptors at global, definition, or runtime level:
770
395
 
771
- **api.ts** - Merge nested structures
772
396
  ```typescript
773
- import { createApiClient, mergeQueries, mergeMutations } from 'vue-api-kit';
774
- import { userApi } from './user-api';
775
- import { postApi } from './post-api';
776
-
777
- export const api = createApiClient({
397
+ // 1. Global interceptor
398
+ const api = createApiClient({
778
399
  baseURL: 'https://api.example.com',
779
-
780
- // Merge nested queries from modules
781
- queries: mergeQueries(userApi.queries, postApi.queries),
782
-
783
- // Merge nested mutations from modules
784
- mutations: mergeMutations(userApi.mutations, postApi.mutations)
400
+ onBeforeRequest: async (config) => {
401
+ config.headers.Authorization = `Bearer ${getToken()}`;
402
+ return config;
403
+ }
785
404
  });
786
405
 
787
- // Usage with nested structure:
788
- api.query.users.getAll() // ✓ Fully typed
789
- api.query.posts.getById() // ✓ Fully typed
790
- api.mutation.users.create() // ✓ Fully typed
791
- api.mutation.posts.create() // Fully typed
792
- ```
793
-
794
- ### Benefits of Modular Approach
795
-
796
- - **Separation of Concerns**: Keep related API endpoints together in dedicated files
797
- - **Reusability**: Import and reuse API definitions across multiple clients
798
- - **Team Collaboration**: Different team members can work on different API modules independently
799
- - **Full Type Safety**: TypeScript infers all types correctly, no loss of type information when merging
800
- - **No Manual Type Assertions**: Use `defineQuery()` and `defineMutation()` helpers instead of `as const`
801
- - **Easy Testing**: Test individual API modules in isolation
802
- - **Better Organization**: Manage large APIs without cluttering a single file
803
-
804
- ## 📤 File Upload Example
805
-
806
- File uploads are supported in mutations using the `isMultipart` flag.
807
-
808
- ```typescript
809
- const api = createApiClient({
810
- baseURL: 'https://api.example.com',
811
- mutations: {
812
- uploadImage: {
813
- method: 'POST',
814
- path: '/upload',
815
- isMultipart: true, // Enable multipart/form-data
816
- response: z.object({
817
- url: z.string()
818
- })
406
+ // 2. Definition-level interceptor
407
+ queries: {
408
+ getUser: {
409
+ path: '/users/{id}',
410
+ onBeforeRequest: async (config) => {
411
+ config.headers['X-Custom-Header'] = 'value';
412
+ return config;
819
413
  }
820
414
  }
821
- });
415
+ }
822
416
 
823
- // In component
824
- const { mutate, uploadProgress } = api.mutation.uploadImage({
825
- onUploadProgress: (progress) => {
826
- console.log(`Upload progress: ${progress}%`);
417
+ // 3. Runtime interceptor
418
+ const { result } = api.query.getUser({
419
+ params: { id: 1 },
420
+ onBeforeRequest: async (config) => {
421
+ config.headers.Authorization = `Bearer ${await refreshToken()}`;
422
+ return config;
827
423
  }
828
424
  });
829
-
830
- async function handleUpload(file: File) {
831
- await mutate({ data: { file } });
832
- }
833
425
  ```
834
426
 
835
- ### Nested Objects in Multipart
427
+ **Execution order:** Global Definition → Runtime
428
+
429
+ ### File Upload
836
430
 
837
- The multipart feature supports nested objects with automatic bracket notation flattening:
431
+ Upload files with multipart/form-data support:
838
432
 
839
433
  ```typescript
840
- const api = createApiClient({
841
- baseURL: 'https://api.example.com',
842
- mutations: {
843
- createProduct: {
844
- method: 'POST',
845
- path: '/products',
846
- isMultipart: true,
847
- response: z.object({
848
- id: z.number(),
849
- success: z.boolean()
850
- })
851
- }
434
+ mutations: {
435
+ uploadImage: {
436
+ method: 'POST',
437
+ path: '/upload',
438
+ isMultipart: true,
439
+ response: z.object({ url: z.string() })
852
440
  }
853
- });
854
-
855
- // In component
856
- const { mutate } = api.mutation.createProduct();
857
-
858
- async function handleSubmit(file: File) {
859
- await mutate({
860
- data: {
861
- code: 'PROD001',
862
- name: 'Product Name',
863
- description: 'Product description',
864
- // Nested objects are automatically flattened with bracket notation
865
- image: {
866
- file_url: 'https://example.com/existing.jpg',
867
- file: file
868
- }
869
- }
870
- });
871
441
  }
872
442
 
873
- // The FormData will be sent as:
874
- // code=PROD001
875
- // name=Product Name
876
- // description=Product description
877
- // image[file_url]=https://example.com/existing.jpg
878
- // image[file]=<File>
879
- ```
443
+ // Usage
444
+ const { mutate, uploadProgress } = api.mutation.uploadImage({
445
+ onUploadProgress: (progress) => console.log(`${progress}%`)
446
+ });
880
447
 
881
- You can also use flat bracket notation directly:
448
+ await mutate({ data: { file, name: 'avatar.jpg' } });
449
+ ```
882
450
 
451
+ **Nested objects in multipart:**
883
452
  ```typescript
884
453
  await mutate({
885
454
  data: {
886
- 'image[file]': file,
887
- 'image[file_url]': 'https://example.com/existing.jpg',
888
- code: 'PROD001'
455
+ name: 'Product',
456
+ image: {
457
+ file: file, // Sent as: image[file]
458
+ file_url: 'url' // Sent as: image[file_url]
459
+ }
889
460
  }
890
461
  });
891
462
  ```
892
463
 
893
- Both approaches work seamlessly together, giving you flexibility in how you structure your data.
894
-
895
- ## 🔒 CSRF Token Protection
896
-
897
- The client includes built-in CSRF token protection, perfect for Laravel Sanctum or similar CSRF-based authentication systems.
464
+ ### CSRF Protection
898
465
 
899
- ### How it works
900
-
901
- **Automatic XSRF Token Handling:**
902
- 1. Set `withCredentials: true` to enable cookie-based authentication
903
- 2. Set `withXSRFToken: true` to enable automatic XSRF token handling
904
- 3. Axios automatically reads `XSRF-TOKEN` cookie and sends it as `X-XSRF-TOKEN` header
905
- 4. This satisfies Laravel Sanctum's CSRF protection requirements
906
-
907
- **Automatic CSRF Refresh:**
908
- 1. Detects CSRF errors (403 or 419 status codes)
909
- 2. Calls the CSRF refresh endpoint to get a new token
910
- 3. Retries the original request automatically with the fresh token
911
- 4. Prevents infinite loops and race conditions
912
-
913
- ### Configuration
466
+ Built-in CSRF token protection (Laravel Sanctum compatible):
914
467
 
915
468
  ```typescript
916
469
  const api = createApiClient({
917
470
  baseURL: 'https://api.example.com',
918
- withCredentials: true, // Enable cookies for authentication
919
- withXSRFToken: true, // Enable automatic XSRF token handling
920
- csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel Sanctum endpoint
921
- queries: { /* ... */ },
471
+ withCredentials: true, // Enable cookies
472
+ withXSRFToken: true, // Enable XSRF token handling
473
+ csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Refresh endpoint
922
474
  mutations: { /* ... */ }
923
475
  });
924
476
  ```
925
477
 
926
- ### Use Case: Laravel Sanctum
927
-
928
- ```typescript
929
- // api.ts
930
- import { createApiClient } from 'vue-api-kit';
931
- import { z } from 'zod';
932
-
933
- export const api = createApiClient({
934
- baseURL: 'https://api.example.com',
935
- withCredentials: true, // Enables cookies
936
- withXSRFToken: true, // Enables automatic XSRF-TOKEN header
937
- csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel's CSRF endpoint mutations: {
938
- login: {
939
- method: 'POST',
940
- path: '/login',
941
- data: z.object({
942
- email: z.string().email(),
943
- password: z.string()
944
- }),
945
- response: z.object({
946
- user: z.object({
947
- id: z.number(),
948
- name: z.string(),
949
- email: z.string()
950
- })
951
- })
952
- },
953
- createPost: {
954
- method: 'POST',
955
- path: '/posts',
956
- data: z.object({
957
- title: z.string(),
958
- content: z.string()
959
- })
960
- }
961
- }
962
- });
478
+ **How it works:**
479
+ 1. Axios automatically reads `XSRF-TOKEN` cookie
480
+ 2. Sends it as `X-XSRF-TOKEN` header
481
+ 3. On 403/419 errors, refreshes CSRF token automatically
482
+ 4. Retries the original request
483
+
484
+ **Laravel CORS config:**
485
+ ```php
486
+ // config/cors.php
487
+ 'supports_credentials' => true,
488
+ 'allowed_origins' => ['http://localhost:5173'],
963
489
  ```
964
490
 
965
- ### Benefits
966
-
967
- - ✅ **Separate Options**: `withCredentials` and `withXSRFToken` can be configured independently
968
- - ✅ **Built-in XSRF Support**: Axios `withXSRFToken` handles token automatically
969
- - ✅ **Automatic Recovery**: No manual token refresh needed
970
- - ✅ **Seamless UX**: Users don't experience authentication errors
971
- - ✅ **Race Condition Safe**: Multiple simultaneous requests share the same refresh
972
- - ✅ **Infinite Loop Prevention**: Won't retry the CSRF endpoint itself
973
- - ✅ **Laravel Sanctum Compatible**: Works perfectly with Laravel's SPA authentication
974
-
975
- ### Important Notes
976
-
977
- 1. **Two separate options**:
978
- - `withCredentials: true` - Enables sending cookies with requests
979
- - `withXSRFToken: true` - Enables automatic XSRF token header handling
980
- 2. **Cookie Domain**: Ensure your API sets cookies with the correct domain (e.g., `.localhost` for local development)
981
- 3. **CORS Configuration**: Your Laravel backend must allow credentials:
982
- ```php
983
- // config/cors.php
984
- 'supports_credentials' => true,
985
- 'allowed_origins' => ['http://localhost:5173'],
986
- ```
987
-
988
-
989
-
990
491
  ## 📝 License
991
492
 
992
493
  MIT
993
494
 
994
495
  ## 👤 Author
995
496
 
996
- MelvishNiz - [GitHub](https://github.com/MelvishNiz)
997
-
497
+ **MelvishNiz** - [GitHub](https://github.com/MelvishNiz)
package/dist/index.js CHANGED
@@ -12,19 +12,20 @@ function X(e) {
12
12
  return e && typeof e == "object" && e !== null && typeof e.path == "string" && typeof e.method == "string";
13
13
  }
14
14
  function F(e, a, h = "") {
15
- if (a instanceof File || a instanceof Blob)
16
- e.append(h, a);
17
- else if (Array.isArray(a))
18
- a.forEach((l) => {
19
- l instanceof File || l instanceof Blob ? e.append(h, l) : typeof l == "object" && l !== null ? F(e, l, h) : e.append(h, String(l));
20
- });
21
- else if (typeof a == "object" && a !== null)
22
- for (const [l, B] of Object.entries(a)) {
23
- const k = h ? `${h}[${l}]` : l;
24
- F(e, B, k);
25
- }
26
- else
27
- e.append(h, String(a));
15
+ if (a !== void 0)
16
+ if (a instanceof File || a instanceof Blob)
17
+ e.append(h, a);
18
+ else if (Array.isArray(a))
19
+ a.forEach((l) => {
20
+ l instanceof File || l instanceof Blob ? e.append(h, l) : typeof l == "object" && l !== null ? F(e, l, h) : l !== void 0 && e.append(h, String(l));
21
+ });
22
+ else if (typeof a == "object" && a !== null)
23
+ for (const [l, B] of Object.entries(a)) {
24
+ const k = h ? `${h}[${l}]` : l;
25
+ F(e, B, k);
26
+ }
27
+ else
28
+ e.append(h, String(a));
28
29
  }
29
30
  function K(e) {
30
31
  const a = U.create({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vue-api-kit",
3
3
  "type": "module",
4
- "version": "1.10.8",
4
+ "version": "1.10.9",
5
5
  "description": "A powerful and flexible API client for Vue 3 applications, built with TypeScript and Zod for type-safe API interactions.",
6
6
  "keywords": [
7
7
  "vue3",