Skip to content

MelvishNiz/vue-api-kit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ vue-api-kit

NPM Version Install Size Bundle Size NPM Downloads CI Status License

A powerful and type-safe API client for Vue 3 applications with built-in validation using Zod.

πŸ“‹ Table of Contents

πŸ“¦ Installation

npm install vue-api-kit

⚑ Quick Start

import { 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>

🎯 Core Features

  • βœ… 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

πŸ“– Basic Usage

Queries (GET)

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>

Queries (POST)

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>

Mutations (POST/PUT/DELETE)

<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>

βš™οΈ Configuration

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: { /* ... */ }
});

πŸ”§ Advanced Features

Nested Structure

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.

Modular API Definitions

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.

Request Interceptors

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

File Upload

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]
    }
  }
});

CSRF Protection

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:

  1. Axios automatically reads XSRF-TOKEN cookie
  2. Sends it as X-XSRF-TOKEN header
  3. On 403/419 errors, refreshes CSRF token automatically
  4. Retries the original request

Laravel CORS config:

// config/cors.php
'supports_credentials' => true,
'allowed_origins' => ['http://localhost:5173'],

πŸ“ License

MIT

πŸ‘€ Author

MelvishNiz - GitHub

About

A powerful and type-safe API client for Vue 3 applications with built-in validation using Zod.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages