🏠 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
| Composable | Purpose |
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 library | 100+ ready composables: npm install @vueuse/core |
📌 Study Checklist
- အပေါ်က concept ကို တစ်ကြောင်းချင်းဖတ်ပြီး example ကို ကိုယ်တိုင်ပြန်ရေးကြည့်ပါ။
- Code/command ပါတဲ့ lesson ဆိုရင် value/name/path တစ်ခုခု ပြောင်းပြီး result ဘာကွာလဲ စမ်းပါ။
- မမှတ်မိသေးတဲ့ keyword 3 ခုကို notebook ထဲရေးပြီး နောက် lesson မသွားခင် ပြန်ရှင်းကြည့်ပါ။
- ပြီးရင် Home / Hub ကိုပြန်သွားပြီး next lesson ဆက်သင်ပါ။