A powerful and type-safe API client for Vue 3 applications with built-in validation using Zod.
npm install vue-api-kitimport { createApiClient } from 'vue-api-kit';
import { z } from 'zod';
// Define your API client
const api = createApiClient({
baseURL: 'https://api.example.com',
queries: {
getUsers: {
path: '/users',
response: z.array(z.object({
id: z.number(),
name: z.string(),
email: z.string()
}))
},
getUser: {
path: '/users/{id}',
params: z.object({ id: z.number() }),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
}
},
mutations: {
createUser: {
method: 'POST',
path: '/users',
data: z.object({
name: z.string(),
email: z.string().email()
}),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
}
}
});Use in your Vue components:
<script setup lang="ts">
import { api } from './api';
// Query - auto-loads on mount
const { result, isLoading, errorMessage } = api.query.getUsers();
// Mutation
const { mutate, isLoading: creating } = api.mutation.createUser();
async function handleCreate() {
await mutate({ name: 'John', email: '[email protected]' });
}
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="errorMessage">Error: {{ errorMessage }}</div>
<ul v-else>
<li v-for="user in result" :key="user.id">{{ user.name }}</li>
</ul>
</template>- β Type-Safe - Full TypeScript support with automatic type inference
- β Zod Validation - Built-in request/response validation
- β Vue 3 Composition API - Reactive state management
- β Lightweight - ~7kB minified (2.2kB gzipped)
- β Auto Loading States - Built-in loading, error, and success states
- β
Path Parameters - Automatic path parameter replacement (
/users/{id}) - β Debouncing - Built-in request debouncing
- β POST Queries - Support both GET and POST for data fetching
- β File Upload - Multipart/form-data with nested objects
- β CSRF Protection - Automatic token refresh (Laravel Sanctum compatible)
- β Modular - Split API definitions across files
- β Nested Structure - Organize endpoints hierarchically
- β Tree-Shakeable - Only bundles what you use
Use queries to fetch data. They automatically load on component mount:
<script setup lang="ts">
import { api } from './api';
import { ref } from 'vue';
// Simple query - automatically loads data on mount
const { result, isLoading, errorMessage } = api.query.getUsers();
// Query with parameters - reactive to parameter changes
const userId = ref(1);
const { result: user, refetch } = api.query.getUser({
params: { id: userId }
});
// Query with options - customize behavior
const { result: data } = api.query.getUsers({
loadOnMount: true,
debounce: 300,
onResult: (data) => console.log('Loaded:', data),
onError: (error) => console.error('Error:', error)
});
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="errorMessage">Error: {{ errorMessage }}</div>
<ul v-else>
<li v-for="user in result" :key="user.id">{{ user.name }}</li>
</ul>
</template>POST queries are perfect for complex searches with filters:
// API definition
queries: {
searchUsers: {
method: 'POST',
path: '/users/search',
data: z.object({
query: z.string(),
filters: z.object({
active: z.boolean().optional(),
role: z.string().optional()
}).optional()
}),
response: z.array(z.object({ id: z.number(), name: z.string() }))
}
}<script setup lang="ts">
const searchTerm = ref('');
const { result, isLoading, refetch } = api.query.searchUsers({
data: {
query: searchTerm.value,
filters: { active: true }
},
loadOnMount: false
});
</script>
<template>
<input v-model="searchTerm" @keyup.enter="refetch" />
<button @click="refetch" :disabled="isLoading">Search</button>
<div v-if="isLoading">Searching...</div>
<div v-else-if="result">
<div v-for="user in result" :key="user.id">{{ user.name }}</div>
</div>
</template><script setup lang="ts">
const { mutate, isLoading, result, errorMessage } = api.mutation.createUser({
onResult: (data) => console.log('Created:', data),
onError: (error) => console.error('Error:', error)
});
const name = ref('');
const email = ref('');
async function handleSubmit() {
await mutate({ name: name.value, email: email.value });
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="name" placeholder="Name" />
<input v-model="email" placeholder="Email" />
<button type="submit" :disabled="isLoading">
{{ isLoading ? 'Creating...' : 'Create User' }}
</button>
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
</form>
</template>const api = createApiClient({
baseURL: 'https://api.example.com',
headers: {
'Authorization': 'Bearer token'
},
withCredentials: true, // Enable cookies
withXSRFToken: true, // Enable XSRF token handling
// CSRF token refresh endpoint
csrfRefreshEndpoint: '/sanctum/csrf-cookie',
// Global handlers
onBeforeRequest: async (config) => {
// Modify requests globally
const token = localStorage.getItem('token');
config.headers.Authorization = `Bearer ${token}`;
return config;
},
onError: (error) => {
// Global error handler
console.error('API Error:', error.message);
},
onZodError: (issues) => {
// Handle validation errors
console.error('Validation errors:', issues);
},
queries: { /* ... */ },
mutations: { /* ... */ }
});Organize endpoints hierarchically for better code organization:
import { createApiClient, defineQuery, defineMutation } from 'vue-api-kit';
import { z } from 'zod';
const api = createApiClient({
baseURL: 'https://api.example.com',
queries: {
users: {
getAll: defineQuery({
path: '/users',
response: z.array(z.object({ id: z.number(), name: z.string() }))
}),
getById: defineQuery({
path: '/users/{id}',
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
}),
search: defineQuery({
method: 'POST',
path: '/users/search',
data: z.object({ query: z.string() }),
response: z.array(z.object({ id: z.number(), name: z.string() }))
})
},
posts: {
getAll: defineQuery({
path: '/posts',
response: z.array(z.object({ id: z.number(), title: z.string() }))
}),
getById: defineQuery({
path: '/posts/{id}',
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), title: z.string() })
})
}
},
mutations: {
users: {
create: defineMutation({
method: 'POST',
path: '/users',
data: z.object({ name: z.string(), email: z.string() }),
response: z.object({ id: z.number(), name: z.string() })
}),
update: defineMutation({
method: 'PUT',
path: '/users/{id}',
params: z.object({ id: z.number() }),
data: z.object({ name: z.string() }),
response: z.object({ id: z.number(), name: z.string() })
}),
delete: defineMutation({
method: 'DELETE',
path: '/users/{id}',
params: z.object({ id: z.number() })
})
}
}
});
// Usage
api.query.users.getAll()
api.mutation.users.create()Benefits: Better organization, namespace separation, improved readability, scalability.
Split your API definitions across multiple files:
user-api.ts
import { defineQuery, defineMutation } from 'vue-api-kit';
import { z } from 'zod';
export const userQueries = {
getUsers: defineQuery({
path: '/users',
response: z.array(z.object({ id: z.number(), name: z.string() }))
}),
getUser: defineQuery({
path: '/users/{id}',
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
})
};
export const userMutations = {
createUser: defineMutation({
method: 'POST',
path: '/users',
data: z.object({ name: z.string(), email: z.string() }),
response: z.object({ id: z.number(), name: z.string() })
})
};api.ts
import { createApiClient, mergeQueries, mergeMutations } from 'vue-api-kit';
import { userQueries, userMutations } from './user-api';
import { postQueries, postMutations } from './post-api';
export const api = createApiClient({
baseURL: 'https://api.example.com',
queries: mergeQueries(userQueries, postQueries),
mutations: mergeMutations(userMutations, postMutations)
});Benefits: Separation of concerns, reusability, team collaboration, full type safety.
Add interceptors at global, definition, or runtime level:
// 1. Global interceptor
const api = createApiClient({
baseURL: 'https://api.example.com',
onBeforeRequest: async (config) => {
config.headers.Authorization = `Bearer ${getToken()}`;
return config;
}
});
// 2. Definition-level interceptor
queries: {
getUser: {
path: '/users/{id}',
onBeforeRequest: async (config) => {
config.headers['X-Custom-Header'] = 'value';
return config;
}
}
}
// 3. Runtime interceptor
const { result } = api.query.getUser({
params: { id: 1 },
onBeforeRequest: async (config) => {
config.headers.Authorization = `Bearer ${await refreshToken()}`;
return config;
}
});Execution order: Global β Definition β Runtime
Upload files with multipart/form-data support:
mutations: {
uploadImage: {
method: 'POST',
path: '/upload',
isMultipart: true,
response: z.object({ url: z.string() })
}
}
// Usage
const { mutate, uploadProgress } = api.mutation.uploadImage({
onUploadProgress: (progress) => console.log(`${progress}%`)
});
await mutate({ data: { file, name: 'avatar.jpg' } });Nested objects in multipart:
await mutate({
data: {
name: 'Product',
image: {
file: file, // Sent as: image[file]
file_url: 'url' // Sent as: image[file_url]
}
}
});Built-in CSRF token protection (Laravel Sanctum compatible):
const api = createApiClient({
baseURL: 'https://api.example.com',
withCredentials: true, // Enable cookies
withXSRFToken: true, // Enable XSRF token handling
csrfRefreshEndpoint: '/sanctum/csrf-cookie', // Refresh endpoint
mutations: { /* ... */ }
});How it works:
- Axios automatically reads
XSRF-TOKENcookie - Sends it as
X-XSRF-TOKENheader - On 403/419 errors, refreshes CSRF token automatically
- Retries the original request
Laravel CORS config:
// config/cors.php
'supports_credentials' => true,
'allowed_origins' => ['http://localhost:5173'],MIT
MelvishNiz - GitHub