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.
- package/README.md +223 -723
- package/dist/index.js +14 -13
- 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
|
[](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://
|
|
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
|
-
|
|
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
|
-
|
|
125
|
+
## 📖 Basic Usage
|
|
100
126
|
|
|
101
|
-
Queries
|
|
127
|
+
### Queries (GET)
|
|
102
128
|
|
|
103
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
|
|
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
|
-
|
|
163
|
+
### Queries (POST)
|
|
164
|
+
|
|
165
|
+
POST queries are perfect for complex searches with filters:
|
|
145
166
|
|
|
146
|
-
|
|
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
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<div v-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
236
|
+
## ⚙️ Configuration
|
|
229
237
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
247
|
+
// CSRF token refresh endpoint
|
|
248
|
+
csrfRefreshEndpoint: '/sanctum/csrf-cookie',
|
|
248
249
|
|
|
249
|
-
|
|
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
|
-
|
|
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()
|
|
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
|
|
323
|
-
|
|
324
|
-
|
|
339
|
+
// Usage
|
|
340
|
+
api.query.users.getAll()
|
|
341
|
+
api.mutation.users.create()
|
|
325
342
|
```
|
|
326
343
|
|
|
327
|
-
|
|
344
|
+
**Benefits:** Better organization, namespace separation, improved readability, scalability.
|
|
328
345
|
|
|
329
|
-
|
|
346
|
+
### Modular API Definitions
|
|
330
347
|
|
|
331
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
390
|
+
**Benefits:** Separation of concerns, reusability, team collaboration, full type safety.
|
|
702
391
|
|
|
703
|
-
|
|
392
|
+
### Request Interceptors
|
|
704
393
|
|
|
705
|
-
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
|
|
781
|
-
|
|
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
|
-
//
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
//
|
|
824
|
-
const {
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
427
|
+
**Execution order:** Global → Definition → Runtime
|
|
428
|
+
|
|
429
|
+
### File Upload
|
|
836
430
|
|
|
837
|
-
|
|
431
|
+
Upload files with multipart/form-data support:
|
|
838
432
|
|
|
839
433
|
```typescript
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
//
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
919
|
-
withXSRFToken: true,
|
|
920
|
-
csrfRefreshEndpoint: '/sanctum/csrf-cookie',
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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