Skip to main content

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

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