Skip to main content

Overview

ByteKit works seamlessly with Vue 3’s Composition API. This guide shows you how to build reactive, type-safe API integrations using Vue composables.

Installation

npm install bytekit

Basic Setup

Create Composables

Create reusable composables for API interactions:
// composables/useApi.ts
import { ref, onMounted, onUnmounted, Ref } from "vue";
import { createApiClient } from "bytekit";

export function useApiClient(config: Parameters<typeof createApiClient>[0]) {
    const client = createApiClient(config);
    return client;
}

export function useApiQuery<T>(
    client: ReturnType<typeof createApiClient>,
    url: string
) {
    const data: Ref<T | null> = ref(null);
    const loading = ref(true);
    const error: Ref<string | null> = ref(null);
    let cancelled = false;

    async function fetchData() {
        try {
            loading.value = true;
            const response = await client.get(url);
            if (!cancelled) {
                data.value = response as T;
                error.value = null;
            }
        } catch (err) {
            if (!cancelled) {
                error.value =
                    err instanceof Error ? err.message : "Unknown error";
            }
        } finally {
            if (!cancelled) {
                loading.value = false;
            }
        }
    }

    onMounted(() => {
        fetchData();
    });

    onUnmounted(() => {
        cancelled = true;
    });

    return { data, loading, error, refetch: fetchData };
}

Complete Example

<template>
    <div class="container">
        <h1>ByteKit + Vue</h1>

        <div class="example">
            <h2>API Client Example</h2>

            <p v-if="loading">Loading...</p>
            <p v-if="error" class="error">Error: {{ error }}</p>
            <pre v-if="data" class="result">{{
                JSON.stringify(data, null, 2)
            }}</pre>
        </div>

        <div class="features">
            <p>✅ Vue Composition API</p>
            <p>✅ Custom composables</p>
            <p>✅ TypeScript ready</p>
        </div>
    </div>
</template>

<script setup lang="ts">
import { useApiClient, useApiQuery } from "./composables/useApi";

interface User {
    id: number;
    name: string;
    email: string;
    phone: string;
    website: string;
}

const client = useApiClient({
    baseUrl: "https://api.example.com",
    timeoutMs: 5000,
    retryPolicy: { maxRetries: 3 },
});

const { data, loading, error } = useApiQuery<User>(client, "/users/1");
</script>

<style scoped>
.container {
    padding: 2rem;
    font-family: system-ui;
}

.example {
    margin-top: 2rem;
}

.error {
    color: red;
}

.result {
    background: #f5f5f5;
    padding: 1rem;
    border-radius: 8px;
    overflow: auto;
}

.features {
    margin-top: 2rem;
    font-size: 0.9rem;
    color: #666;
}
</style>

Using QueryClient

Integrate ByteKit’s QueryClient for advanced caching:
// composables/useQueryClient.ts
import { ref, onMounted, onUnmounted } from "vue";
import { createApiClient, createQueryClient } from "bytekit";

const apiClient = createApiClient({
    baseUrl: "https://api.example.com",
});

const queryClient = createQueryClient(apiClient, {
    defaultStaleTime: 5000,
    defaultCacheTime: 60000,
});

export function useQuery<T>(queryKey: string[], path: string) {
    const data = ref<T | null>(null);
    const loading = ref(true);
    const error = ref<Error | null>(null);
    let cancelled = false;

    async function fetch() {
        try {
            loading.value = true;
            const result = await queryClient.query({ queryKey, path });
            if (!cancelled) {
                data.value = result as T;
                error.value = null;
            }
        } catch (err) {
            if (!cancelled) {
                error.value = err as Error;
            }
        } finally {
            if (!cancelled) {
                loading.value = false;
            }
        }
    }

    onMounted(() => {
        fetch();
    });

    onUnmounted(() => {
        cancelled = true;
    });

    return { data, loading, error, refetch: fetch };
}

Usage with QueryClient

<template>
    <div>
        <h2 v-if="loading">Loading...</h2>
        <div v-else-if="error">Error: {{ error.message }}</div>
        <div v-else-if="data">
            <h2>{{ data.name }}</h2>
            <p>{{ data.email }}</p>
            <button @click="refetch">Refresh</button>
        </div>
    </div>
</template>

<script setup lang="ts">
import { useQuery } from "./composables/useQueryClient";

interface User {
    id: number;
    name: string;
    email: string;
}

const props = defineProps<{ userId: string }>();

const { data, loading, error, refetch } = useQuery<User>(
    ["user", props.userId],
    `/users/${props.userId}`
);
</script>

State Management Patterns

Global API Client with Provide/Inject

// main.ts
import { createApp } from "vue";
import { createApiClient } from "bytekit";
import App from "./App.vue";

const apiClient = createApiClient({
    baseUrl: "https://api.example.com",
    timeoutMs: 5000,
    retryPolicy: { maxRetries: 3 },
});

const app = createApp(App);
app.provide("apiClient", apiClient);
app.mount("#app");
// In components
import { inject } from "vue";
import type { ApiClient } from "bytekit";

export function useApi() {
    const client = inject<ReturnType<typeof createApiClient>>("apiClient");
    if (!client) {
        throw new Error("API client not provided");
    }
    return client;
}

Mutations Composable

// composables/useMutation.ts
import { ref } from "vue";
import type { ApiClient } from "bytekit";

export function useMutation<T, V>(
    client: ReturnType<typeof createApiClient>
) {
    const loading = ref(false);
    const error = ref<string | null>(null);
    const data = ref<T | null>(null);

    const mutate = async (url: string, body: V, method = "POST") => {
        loading.value = true;
        error.value = null;

        try {
            const response = await client.request({
                url,
                method,
                body,
            });
            data.value = response as T;
            return response as T;
        } catch (err) {
            error.value = err instanceof Error ? err.message : "Unknown error";
            throw err;
        } finally {
            loading.value = false;
        }
    };

    return { mutate, loading, error, data };
}

Usage Example

<template>
    <form @submit.prevent="handleSubmit">
        <input v-model="formData.name" placeholder="Name" />
        <input v-model="formData.email" placeholder="Email" />
        
        <p v-if="error" class="error">{{ error }}</p>
        
        <button :disabled="loading">
            {{ loading ? "Creating..." : "Create User" }}
        </button>
    </form>
</template>

<script setup lang="ts">
import { reactive } from "vue";
import { useApi } from "./composables/useApi";
import { useMutation } from "./composables/useMutation";

interface User {
    id: number;
    name: string;
    email: string;
}

const client = useApi();
const { mutate, loading, error } = useMutation<User, Partial<User>>(client);

const formData = reactive({
    name: "",
    email: "",
});

const handleSubmit = async () => {
    try {
        const newUser = await mutate("/users", formData);
        console.log("Created:", newUser);
        // Reset form
        formData.name = "";
        formData.email = "";
    } catch (err) {
        // Error is already in state
    }
};
</script>

Best Practices

Prefer Composition API over Options API for better type inference and code organization:
<script setup lang="ts">
// Better type safety and less boilerplate
import { useApiQuery } from "./composables/useApi";
</script>
Use ref() for primitive values and reactive() for objects:
const loading = ref(true); // for booleans
const formData = reactive({ name: "", email: "" }); // for objects
Always cleanup in onUnmounted to prevent memory leaks:
onUnmounted(() => {
    cancelled = true;
});
Use TypeScript generics for type-safe responses:
const { data } = useApiQuery<User>(client, "/users/1");
// data is typed as Ref<User | null>

Next Steps

API Client Guide

Learn more about ApiClient configuration

State Management

Explore QueryClient for advanced caching

TypeScript Support

Generate types from OpenAPI specs

Error Handling

Best practices for error handling

Build docs developers (and LLMs) love