🏠 Home / Hub

📡 Vue 16 — API Calls & Composables

← Vue Menu · ← Prev: Pinia

1. Axios Setup (Recommended over fetch for apps)

npm install axios

# src/services/api.js
import axios from 'axios'
import { useAuthStore } from '@/stores/useAuthStore'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' },
})

// Request interceptor — attach auth token
api.interceptors.request.use(config => {
  const auth = useAuthStore()
  if (auth.token) {
    config.headers.Authorization = `Bearer ${auth.token}`
  }
  return config
})

// Response interceptor — handle 401
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      const auth = useAuthStore()
      auth.logout()
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export default api

2. useFetch Composable — Generic Data Fetching

// composables/useFetch.js
import { ref } from 'vue'
import api from '@/services/api'

export function useFetch(url, options = {}) {
  const data    = ref(options.initialData ?? null)
  const loading = ref(false)
  const error   = ref(null)

  async function execute(overrideUrl) {
    loading.value = true
    error.value   = null
    try {
      const res = await api.get(overrideUrl ?? url)
      data.value = res.data
    } catch (e) {
      error.value = e.response?.data?.message || e.message || 'Request failed'
    } finally {
      loading.value = false
    }
  }

  if (options.immediate !== false) execute()

  return { data, loading, error, execute }
}

// Usage in component:
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data: products, loading, error } = useFetch('/products')
// data is fetched immediately on mount
</script>

3. useCrud Composable — Full CRUD

// composables/useCrud.js
import { ref } from 'vue'
import api from '@/services/api'

export function useCrud(resource) {
  const items   = ref([])
  const loading = ref(false)
  const error   = ref(null)

  const handleError = (e) => {
    error.value = e.response?.data?.message || e.message
    throw e
  }

  async function fetchAll(params = {}) {
    loading.value = true
    error.value   = null
    try {
      const res = await api.get(`/${resource}`, { params })
      items.value = res.data.data ?? res.data
    } catch (e) { handleError(e) }
    finally { loading.value = false }
  }

  async function create(data) {
    const res = await api.post(`/${resource}`, data).catch(handleError)
    items.value.push(res.data)
    return res.data
  }

  async function update(id, data) {
    const res = await api.put(`/${resource}/${id}`, data).catch(handleError)
    const idx = items.value.findIndex(i => i.id === id)
    if (idx !== -1) items.value[idx] = res.data
    return res.data
  }

  async function remove(id) {
    await api.delete(`/${resource}/${id}`).catch(handleError)
    items.value = items.value.filter(i => i.id !== id)
  }

  return { items, loading, error, fetchAll, create, update, remove }
}

// Usage:
const { items: products, loading, fetchAll, create, update, remove } = useCrud('products')
await fetchAll({ search: 'phone', page: 1 })

4. Pagination Composable

// composables/usePagination.js
import { ref, computed } from 'vue'
import api from '@/services/api'

export function usePagination(resource) {
  const items     = ref([])
  const page      = ref(1)
  const perPage   = ref(15)
  const total     = ref(0)
  const loading   = ref(false)

  const totalPages = computed(() => Math.ceil(total.value / perPage.value))
  const hasNext    = computed(() => page.value < totalPages.value)
  const hasPrev    = computed(() => page.value > 1)

  async function fetch(params = {}) {
    loading.value = true
    try {
      const res = await api.get(`/${resource}`, {
        params: { page: page.value, per_page: perPage.value, ...params }
      })
      items.value = res.data.data
      total.value = res.data.meta?.total ?? res.data.total ?? 0
    } finally {
      loading.value = false
    }
  }

  function nextPage() { if (hasNext.value) { page.value++; fetch() } }
  function prevPage() { if (hasPrev.value) { page.value--; fetch() } }
  function goToPage(n) { page.value = n; fetch() }

  return { items, page, perPage, total, totalPages, hasNext, hasPrev,
           loading, fetch, nextPage, prevPage, goToPage }
}

5. useDebounce — Delayed Search

// composables/useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 300) {
  const debounced = ref(value.value)
  let timeout

  watch(value, (v) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => { debounced.value = v }, delay)
  })

  return debounced
}

// Usage: search input that triggers API after user stops typing
<script setup>
import { ref, watch } from 'vue'
import { useDebounce } from '@/composables/useDebounce'
import { useCrud }     from '@/composables/useCrud'

const search = ref('')
const debouncedSearch = useDebounce(search, 400)
const { items: products, fetchAll } = useCrud('products')

watch(debouncedSearch, (q) => fetchAll({ search: q }), { immediate: true })
</script>

<template>
  <input v-model="search" placeholder="Search...">
  <!-- fetchAll triggers 400ms after user stops typing -->
</template>

6. Error Handling Patterns

// Centralized error handler
export function handleApiError(error) {
  const status  = error.response?.status
  const message = error.response?.data?.message || error.message

  if (status === 422) {
    // Validation errors — Laravel returns {errors: {field: [messages]}}
    return { type: 'validation', errors: error.response.data.errors }
  }
  if (status === 401) return { type: 'unauthenticated' }
  if (status === 403) return { type: 'forbidden' }
  if (status === 404) return { type: 'not-found', message }
  if (status >= 500) return { type: 'server', message }

  return { type: 'unknown', message }
}

// In component — display field errors from Laravel 422
<template>
  <form @submit.prevent="submit">
    <input v-model="form.email">
    <span v-if="errors.email" class="error">{{ errors.email[0] }}</span>
    <input v-model="form.password">
    <span v-if="errors.password" class="error">{{ errors.password[0] }}</span>
  </form>
</template>

<script setup>
const errors = ref({})
async function submit() {
  try {
    await authStore.login(form.email, form.password)
  } catch (e) {
    if (e.response?.status === 422) {
      errors.value = e.response.data.errors   // field-level errors
    }
  }
}
</script>

7. Common Composables Reference

ComposablePurpose
useFetch(url)Generic GET + loading/error
useCrud(resource)Full CRUD for a REST resource
usePagination(resource)Paginated list with next/prev
useDebounce(value, ms)Delayed reactive value
useLocalStorage(key, default)Persisted ref
useClipboard()Copy to clipboard
useWindowSize()Reactive window width/height
useIntersectionObserver()Detect element visibility
VueUse library100+ ready composables: npm install @vueuse/core

📌 Study Checklist