Documentation Index
Fetch the complete documentation index at: https://docs-mstore.faisalaffan.com/llms.txt
Use this file to discover all available pages before exploring further.
Data Flow Patterns
Dokumentasi lengkap tentang alur data dari UI layer hingga backend API di MStore Dashboard.
Overview
Layer Responsibilities
1. Page Component
Responsibility: Render UI dan handle user interactions
<!-- app/pages/inventory/index.vue -->
<script setup lang="ts">
import { useInventoryList, InventoryTable } from '~/features/03_inventory'
// Use composable for page logic
const {
items,
loading,
error,
searchQuery,
fetchItems,
} = useInventoryList()
// Lifecycle
onMounted(() => {
fetchItems()
})
// Event handlers
const handleSearch = (query: string) => {
searchQuery.value = query
}
</script>
<template>
<div class="p-6">
<UInput
v-model="searchQuery"
placeholder="Search inventory..."
@input="handleSearch"
/>
<UAlert v-if="error" color="red" :title="error" />
<InventoryTable
:items="items"
:loading="loading"
/>
</div>
</template>
2. Composable
Responsibility: Page-specific logic, koordinasi antara UI dan store
// features/03_inventory/composables/useInventoryList.ts
export const useInventoryList = () => {
const store = useInventoryStore()
// Local page state
const searchQuery = ref('')
const sortBy = ref<'name' | 'price' | 'stock'>('name')
const sortOrder = ref<'asc' | 'desc'>('asc')
// Computed from store (reactive)
const items = computed(() => store.items)
const loading = computed(() => store.loading)
const error = computed(() => store.error)
// Local computed (derived from local + store state)
const sortedItems = computed(() => {
const sorted = [...items.value]
sorted.sort((a, b) => {
const aVal = a[sortBy.value]
const bVal = b[sortBy.value]
if (sortOrder.value === 'asc') {
return aVal > bVal ? 1 : -1
}
return aVal < bVal ? 1 : -1
})
return sorted
})
// Actions
const fetchItems = async () => {
await store.fetchItems({
search: searchQuery.value || undefined,
})
}
// Watch for search changes with debounce
const debouncedFetch = useDebounceFn(fetchItems, 300)
watch(searchQuery, () => {
debouncedFetch()
})
return {
// Expose state
searchQuery,
sortBy,
sortOrder,
// Expose computed
items: sortedItems,
loading,
error,
// Expose actions
fetchItems,
}
}
3. Pinia Store
Responsibility: Global state management, API coordination
// features/03_inventory/store/inventory.store.ts
import { defineStore } from 'pinia'
import { getInventoryList, createInventory, updateInventory, deleteInventory } from '../api'
import type { Inventory, InventoryQueryParams, InventoryResponse } from '../types'
export const useInventoryStore = defineStore('inventory', () => {
// State
const items = ref<Inventory[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const meta = ref({
total: 0,
page: 1,
limit: 20,
})
// Getters
const getById = computed(() => (id: string) =>
items.value.find(item => item.id === id)
)
const lowStockItems = computed(() =>
items.value.filter(item => item.stock < item.minStock)
)
// Actions
const fetchItems = async (params?: InventoryQueryParams) => {
loading.value = true
error.value = null
try {
const response = await getInventoryList(params)
items.value = response.data
meta.value = response.meta
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch inventory'
throw e
} finally {
loading.value = false
}
}
const addItem = async (data: Omit<Inventory, 'id' | 'createdAt'>) => {
loading.value = true
try {
const newItem = await createInventory(data)
items.value.push(newItem)
return newItem
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to create item'
throw e
} finally {
loading.value = false
}
}
const updateItem = async (id: string, data: Partial<Inventory>) => {
const index = items.value.findIndex(item => item.id === id)
if (index === -1) throw new Error('Item not found')
try {
const updated = await updateInventory(id, data)
items.value[index] = updated
return updated
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to update item'
throw e
}
}
const removeItem = async (id: string) => {
try {
await deleteInventory(id)
items.value = items.value.filter(item => item.id !== id)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to delete item'
throw e
}
}
const $reset = () => {
items.value = []
loading.value = false
error.value = null
meta.value = { total: 0, page: 1, limit: 20 }
}
return {
// State
items,
loading,
error,
meta,
// Getters
getById,
lowStockItems,
// Actions
fetchItems,
addItem,
updateItem,
removeItem,
$reset,
}
})
4. API Layer
Responsibility: HTTP communication dengan backend
// features/03_inventory/api/getList.ts
import type { Inventory, InventoryQueryParams, InventoryResponse } from '../types'
export const getInventoryList = async (
params?: InventoryQueryParams
): Promise<InventoryResponse> => {
const query = new URLSearchParams()
if (params?.page) query.set('page', params.page.toString())
if (params?.limit) query.set('limit', params.limit.toString())
if (params?.search) query.set('search', params.search)
if (params?.category) query.set('category', params.category)
if (params?.sortBy) query.set('sortBy', params.sortBy)
if (params?.sortOrder) query.set('sortOrder', params.sortOrder)
return await $fetch<InventoryResponse>('/api/v1/inventory', {
method: 'GET',
query: Object.fromEntries(query),
})
}
// features/03_inventory/api/create.ts
import type { Inventory, CreateInventoryDto } from '../types'
export const createInventory = async (
data: CreateInventoryDto
): Promise<Inventory> => {
return await $fetch<Inventory>('/api/v1/inventory', {
method: 'POST',
body: data,
})
}
// features/03_inventory/api/update.ts
import type { Inventory, UpdateInventoryDto } from '../types'
export const updateInventory = async (
id: string,
data: UpdateInventoryDto
): Promise<Inventory> => {
return await $fetch<Inventory>(`/api/v1/inventory/${id}`, {
method: 'PUT',
body: data,
})
}
// features/03_inventory/api/delete.ts
export const deleteInventory = async (id: string): Promise<void> => {
await $fetch(`/api/v1/inventory/${id}`, {
method: 'DELETE',
})
}
Complete Data Flow Example
Creating New Item
Fetching with Search
Error Handling Flow
// Composable level - user-friendly error handling
const useInventoryList = () => {
const store = useInventoryStore()
const toast = useToast()
const fetchItems = async () => {
try {
await store.fetchItems()
} catch (error) {
// Show user-friendly message
toast.add({
title: 'Error',
description: 'Failed to load inventory. Please try again.',
color: 'red',
})
}
}
return { fetchItems, error: computed(() => store.error) }
}
// Store level - state management
const fetchItems = async (params?: InventoryQueryParams) => {
loading.value = true
error.value = null
try {
const response = await getInventoryList(params)
items.value = response.data
} catch (e) {
// Store error for UI display
error.value = e instanceof Error ? e.message : 'Unknown error'
// Re-throw for composable to handle
throw e
} finally {
loading.value = false
}
}
// API level - HTTP error handling
export const getInventoryList = async (params?: InventoryQueryParams) => {
try {
return await $fetch<InventoryResponse>('/api/v1/inventory', {
method: 'GET',
query: params,
})
} catch (error) {
// Transform API errors
if (error instanceof FetchError) {
if (error.status === 401) {
throw new Error('Please login to continue')
}
if (error.status === 403) {
throw new Error('You do not have permission to view inventory')
}
if (error.status === 500) {
throw new Error('Server error. Please try again later.')
}
}
throw error
}
}
Optimistic Updates
Untuk UX yang lebih baik, update UI sebelum server response:
// Store with optimistic update
const removeItem = async (id: string) => {
// Save current state for rollback
const previousItems = [...items.value]
// Optimistic update
items.value = items.value.filter(item => item.id !== id)
try {
await deleteInventory(id)
} catch (e) {
// Rollback on error
items.value = previousItems
error.value = 'Failed to delete item'
throw e
}
}
Best Practices
- Store adalah single source of truth untuk domain state
- Composables hanya membaca dari store
- Components tidak langsung memodifikasi store state
- API layer: Transform HTTP errors ke user-friendly messages
- Store: Simpan error state untuk UI
- Composable: Handle errors dengan toast/notifications
- Component: Display error UI
- Gunakan loading state di store
- Show skeleton/spinner saat loading
- Disable form buttons saat submitting
- Prevent double-submission
- Validate di form level (composable)
- Validate lagi di API level (backend)
- Show validation errors per field
- Clear errors on input change
Next Steps
API Interceptors
JWT auto-refresh dan error handling
Authentication
Complete auth flow