vue-api-kit 1.10.7 → 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.
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:
98
84
 
99
- ### Queries (GET and POST requests)
85
+ ```vue
86
+ <script setup lang="ts">
87
+ import { api } from './api';
100
88
 
101
- Queries support both GET and POST methods, allowing you to fetch data with complex search criteria.
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
124
+
125
+ ## 📖 Basic Usage
102
126
 
103
- #### GET Queries
127
+ ### Queries (GET)
128
+
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)
145
164
 
146
- POST queries are perfect for complex searches, filtering, or any operation that requires sending data in the request body.
165
+ POST queries are perfect for complex searches with filters:
166
+
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 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
+ },
250
257
 
251
- ### Basic Nested Structure
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
276
+
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();
325
- ```
326
-
327
- ### Deep Nesting
328
-
329
- You can nest as deeply as needed for complex API structures:
330
-
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
- onErrorRequest: (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
- });
339
+ // Usage
340
+ api.query.users.getAll()
341
+ api.mutation.users.create()
450
342
  ```
451
343
 
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
344
+ **Benefits:** Better organization, namespace separation, improved readability, scalability.
457
345
 
458
- You can define `onBeforeRequest` in two ways for queries:
346
+ ### Modular API Definitions
459
347
 
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,326 +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';
394
+ Add interceptors at global, definition, or runtime level:
708
395
 
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
396
  ```typescript
743
- import { z, defineQuery, defineMutation } from 'vue-api-kit';
397
+ // 1. Global interceptor
398
+ const api = createApiClient({
399
+ baseURL: 'https://api.example.com',
400
+ onBeforeRequest: async (config) => {
401
+ config.headers.Authorization = `Bearer ${getToken()}`;
402
+ return config;
403
+ }
404
+ });
744
405
 
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
- })
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;
766
413
  }
767
414
  }
768
- };
769
- ```
770
-
771
- **api.ts** - Merge nested structures
772
- ```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({
778
- baseURL: 'https://api.example.com',
779
-
780
- // Merge nested queries from modules
781
- queries: mergeQueries(userApi.queries, postApi.queries),
415
+ }
782
416
 
783
- // Merge nested mutations from modules
784
- mutations: mergeMutations(userApi.mutations, postApi.mutations)
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;
423
+ }
785
424
  });
786
-
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
425
  ```
793
426
 
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
427
+ **Execution order:** Global Definition → Runtime
803
428
 
804
- ## 📤 File Upload Example
429
+ ### File Upload
805
430
 
806
- File uploads are supported in mutations using the `isMultipart` flag.
431
+ Upload files with multipart/form-data support:
807
432
 
808
433
  ```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
- })
819
- }
434
+ mutations: {
435
+ uploadImage: {
436
+ method: 'POST',
437
+ path: '/upload',
438
+ isMultipart: true,
439
+ response: z.object({ url: z.string() })
820
440
  }
821
- });
441
+ }
822
442
 
823
- // In component
443
+ // Usage
824
444
  const { mutate, uploadProgress } = api.mutation.uploadImage({
825
- onUploadProgress: (progress) => {
826
- console.log(`Upload progress: ${progress}%`);
827
- }
445
+ onUploadProgress: (progress) => console.log(`${progress}%`)
828
446
  });
829
447
 
830
- async function handleUpload(file: File) {
831
- await mutate({ data: { file } });
832
- }
448
+ await mutate({ data: { file, name: 'avatar.jpg' } });
833
449
  ```
834
450
 
835
- ## 🔒 CSRF Token Protection
836
-
837
- The client includes built-in CSRF token protection, perfect for Laravel Sanctum or similar CSRF-based authentication systems.
838
-
839
- ### How it works
840
-
841
- **Automatic XSRF Token Handling:**
842
- 1. Set `withCredentials: true` to enable cookie-based authentication
843
- 2. Set `withXSRFToken: true` to enable automatic XSRF token handling
844
- 3. Axios automatically reads `XSRF-TOKEN` cookie and sends it as `X-XSRF-TOKEN` header
845
- 4. This satisfies Laravel Sanctum's CSRF protection requirements
451
+ **Nested objects in multipart:**
452
+ ```typescript
453
+ await mutate({
454
+ data: {
455
+ name: 'Product',
456
+ image: {
457
+ file: file, // Sent as: image[file]
458
+ file_url: 'url' // Sent as: image[file_url]
459
+ }
460
+ }
461
+ });
462
+ ```
846
463
 
847
- **Automatic CSRF Refresh:**
848
- 1. Detects CSRF errors (403 or 419 status codes)
849
- 2. Calls the CSRF refresh endpoint to get a new token
850
- 3. Retries the original request automatically with the fresh token
851
- 4. Prevents infinite loops and race conditions
464
+ ### CSRF Protection
852
465
 
853
- ### Configuration
466
+ Built-in CSRF token protection (Laravel Sanctum compatible):
854
467
 
855
468
  ```typescript
856
469
  const api = createApiClient({
857
470
  baseURL: 'https://api.example.com',
858
- withCredentials: true, // Enable cookies for authentication
859
- withXSRFToken: true, // Enable automatic XSRF token handling
860
- csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel Sanctum endpoint
861
- queries: { /* ... */ },
471
+ withCredentials: true, // Enable cookies
472
+ withXSRFToken: true, // Enable XSRF token handling
473
+ csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Refresh endpoint
862
474
  mutations: { /* ... */ }
863
475
  });
864
476
  ```
865
477
 
866
- ### Use Case: Laravel Sanctum
867
-
868
- ```typescript
869
- // api.ts
870
- import { createApiClient } from 'vue-api-kit';
871
- import { z } from 'zod';
872
-
873
- export const api = createApiClient({
874
- baseURL: 'https://api.example.com',
875
- withCredentials: true, // Enables cookies
876
- withXSRFToken: true, // Enables automatic XSRF-TOKEN header
877
- csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Laravel's CSRF endpoint mutations: {
878
- login: {
879
- method: 'POST',
880
- path: '/login',
881
- data: z.object({
882
- email: z.string().email(),
883
- password: z.string()
884
- }),
885
- response: z.object({
886
- user: z.object({
887
- id: z.number(),
888
- name: z.string(),
889
- email: z.string()
890
- })
891
- })
892
- },
893
- createPost: {
894
- method: 'POST',
895
- path: '/posts',
896
- data: z.object({
897
- title: z.string(),
898
- content: z.string()
899
- })
900
- }
901
- }
902
- });
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'],
903
489
  ```
904
490
 
905
- ### Benefits
906
-
907
- - ✅ **Separate Options**: `withCredentials` and `withXSRFToken` can be configured independently
908
- - ✅ **Built-in XSRF Support**: Axios `withXSRFToken` handles token automatically
909
- - ✅ **Automatic Recovery**: No manual token refresh needed
910
- - ✅ **Seamless UX**: Users don't experience authentication errors
911
- - ✅ **Race Condition Safe**: Multiple simultaneous requests share the same refresh
912
- - ✅ **Infinite Loop Prevention**: Won't retry the CSRF endpoint itself
913
- - ✅ **Laravel Sanctum Compatible**: Works perfectly with Laravel's SPA authentication
914
-
915
- ### Important Notes
916
-
917
- 1. **Two separate options**:
918
- - `withCredentials: true` - Enables sending cookies with requests
919
- - `withXSRFToken: true` - Enables automatic XSRF token header handling
920
- 2. **Cookie Domain**: Ensure your API sets cookies with the correct domain (e.g., `.localhost` for local development)
921
- 3. **CORS Configuration**: Your Laravel backend must allow credentials:
922
- ```php
923
- // config/cors.php
924
- 'supports_credentials' => true,
925
- 'allowed_origins' => ['http://localhost:5173'],
926
- ```
927
-
928
-
929
-
930
491
  ## 📝 License
931
492
 
932
493
  MIT
933
494
 
934
495
  ## 👤 Author
935
496
 
936
- MelvishNiz - [GitHub](https://github.com/MelvishNiz)
937
-
497
+ **MelvishNiz** - [GitHub](https://github.com/MelvishNiz)