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.

API Layer Pattern

Setiap feature module memiliki API layer yang terorganisir untuk menangani komunikasi HTTP dengan backend.

API Layer Structure

features/{domain}/api/
├── index.ts              # Barrel export
├── getList.ts            # GET collection
├── getById.ts            # GET single item
├── create.ts             # POST create
├── update.ts             # PUT/PATCH update
├── delete.ts             # DELETE
└── {custom}.ts           # Custom endpoints

Standard CRUD Operations

Get List (Collection)

// features/03_inventory/api/getList.ts
import type { Inventory, InventoryQueryParams, PaginatedResponse } from '../types'

export const getInventoryList = async (
  params?: InventoryQueryParams
): Promise<PaginatedResponse<Inventory>> => {
  const { $api } = useNuxtApp()

  // Build query params
  const query: Record<string, string | number | undefined> = {}

  if (params?.page) query.page = params.page
  if (params?.limit) query.limit = params.limit
  if (params?.search) query.search = params.search
  if (params?.sortBy) query.sortBy = params.sortBy
  if (params?.sortOrder) query.sortOrder = params.sortOrder
  if (params?.category) query.category = params.category
  if (params?.status) query.status = params.status

  return await $api<PaginatedResponse<Inventory>>('/api/v1/inventory', {
    method: 'GET',
    query,
  })
}

Get By ID (Single Item)

// features/03_inventory/api/getById.ts
import type { Inventory } from '../types'

export const getInventoryById = async (id: string): Promise<Inventory> => {
  const { $api } = useNuxtApp()

  return await $api<Inventory>(`/api/v1/inventory/${id}`, {
    method: 'GET',
  })
}

Create (POST)

// features/03_inventory/api/create.ts
import type { Inventory, CreateInventoryDto } from '../types'

export const createInventory = async (
  data: CreateInventoryDto
): Promise<Inventory> => {
  const { $api } = useNuxtApp()

  return await $api<Inventory>('/api/v1/inventory', {
    method: 'POST',
    body: data,
  })
}

Update (PUT/PATCH)

// features/03_inventory/api/update.ts
import type { Inventory, UpdateInventoryDto } from '../types'

export const updateInventory = async (
  id: string,
  data: UpdateInventoryDto
): Promise<Inventory> => {
  const { $api } = useNuxtApp()

  return await $api<Inventory>(`/api/v1/inventory/${id}`, {
    method: 'PUT',
    body: data,
  })
}

// Partial update
export const patchInventory = async (
  id: string,
  data: Partial<UpdateInventoryDto>
): Promise<Inventory> => {
  const { $api } = useNuxtApp()

  return await $api<Inventory>(`/api/v1/inventory/${id}`, {
    method: 'PATCH',
    body: data,
  })
}

Delete

// features/03_inventory/api/delete.ts
export const deleteInventory = async (id: string): Promise<void> => {
  const { $api } = useNuxtApp()

  await $api(`/api/v1/inventory/${id}`, {
    method: 'DELETE',
  })
}

// Bulk delete
export const bulkDeleteInventory = async (ids: string[]): Promise<void> => {
  const { $api } = useNuxtApp()

  await $api('/api/v1/inventory/bulk-delete', {
    method: 'POST',
    body: { ids },
  })
}

Barrel Export

// features/03_inventory/api/index.ts
export { getInventoryList } from './getList'
export { getInventoryById } from './getById'
export { createInventory } from './create'
export { updateInventory, patchInventory } from './update'
export { deleteInventory, bulkDeleteInventory } from './delete'

// Custom exports
export { importInventory } from './import'
export { exportInventory } from './export'

Type Definitions

// features/03_inventory/types/inventory.types.ts

// Entity
export interface Inventory {
  id: string
  name: string
  sku: string
  price: number
  stock: number
  minStock: number
  category: string
  status: 'active' | 'inactive'
  createdAt: string
  updatedAt: string
}

// Query params
export interface InventoryQueryParams {
  page?: number
  limit?: number
  search?: string
  sortBy?: keyof Inventory
  sortOrder?: 'asc' | 'desc'
  category?: string
  status?: 'active' | 'inactive'
}

// Create DTO (Data Transfer Object)
export interface CreateInventoryDto {
  name: string
  sku: string
  price: number
  stock: number
  minStock?: number
  category: string
}

// Update DTO
export interface UpdateInventoryDto {
  name?: string
  sku?: string
  price?: number
  stock?: number
  minStock?: number
  category?: string
  status?: 'active' | 'inactive'
}

// Paginated response
export interface PaginatedResponse<T> {
  data: T[]
  meta: {
    total: number
    page: number
    limit: number
    totalPages: number
  }
}

Custom API Functions

Import/Export

// features/03_inventory/api/import.ts
export interface ImportResult {
  success: number
  failed: number
  errors: Array<{
    row: number
    message: string
  }>
}

export const importInventory = async (file: File): Promise<ImportResult> => {
  const { $api } = useNuxtApp()

  const formData = new FormData()
  formData.append('file', file)

  return await $api<ImportResult>('/api/v1/inventory/import', {
    method: 'POST',
    body: formData,
  })
}

export const validateImport = async (file: File): Promise<{
  valid: boolean
  errors: string[]
  preview: Inventory[]
}> => {
  const { $api } = useNuxtApp()

  const formData = new FormData()
  formData.append('file', file)

  return await $api('/api/v1/inventory/import/validate', {
    method: 'POST',
    body: formData,
  })
}
// features/03_inventory/api/export.ts
export const exportInventory = async (
  format: 'xlsx' | 'csv',
  params?: InventoryQueryParams
): Promise<Blob> => {
  const { $api } = useNuxtApp()

  return await $api<Blob>('/api/v1/inventory/export', {
    method: 'GET',
    query: { format, ...params },
    responseType: 'blob',
  })
}

Stock Operations

// features/03_inventory/api/stock.ts
export interface StockAdjustment {
  productId: string
  quantity: number
  type: 'in' | 'out' | 'adjustment'
  reason: string
  reference?: string
}

export const adjustStock = async (
  data: StockAdjustment
): Promise<Inventory> => {
  const { $api } = useNuxtApp()

  return await $api<Inventory>('/api/v1/inventory/stock/adjust', {
    method: 'POST',
    body: data,
  })
}

export const transferStock = async (data: {
  productId: string
  quantity: number
  fromWarehouse: string
  toWarehouse: string
}): Promise<void> => {
  const { $api } = useNuxtApp()

  await $api('/api/v1/inventory/stock/transfer', {
    method: 'POST',
    body: data,
  })
}

Error Handling in API Layer

// features/03_inventory/api/getList.ts
import type { Inventory, InventoryQueryParams, PaginatedResponse } from '../types'

export const getInventoryList = async (
  params?: InventoryQueryParams
): Promise<PaginatedResponse<Inventory>> => {
  const { $api } = useNuxtApp()

  try {
    return await $api<PaginatedResponse<Inventory>>('/api/v1/inventory', {
      method: 'GET',
      query: params,
    })
  } catch (error) {
    // Transform specific errors
    if (error instanceof FetchError) {
      if (error.statusCode === 403) {
        throw new Error('You do not have permission to view inventory')
      }
      if (error.statusCode === 404) {
        throw new Error('Inventory not found')
      }
    }

    // Re-throw for store/composable to handle
    throw error
  }
}

API with Caching

// features/03_inventory/api/getList.ts
export const getInventoryList = async (
  params?: InventoryQueryParams,
  options?: { cache?: boolean }
): Promise<PaginatedResponse<Inventory>> => {
  const { $api } = useNuxtApp()

  // Use Nuxt's built-in caching for GET requests
  if (options?.cache) {
    const cacheKey = `inventory-list-${JSON.stringify(params)}`

    return await useAsyncData(cacheKey, () =>
      $api<PaginatedResponse<Inventory>>('/api/v1/inventory', {
        query: params,
      })
    ).then(({ data }) => data.value!)
  }

  return await $api<PaginatedResponse<Inventory>>('/api/v1/inventory', {
    query: params,
  })
}

Using API in Store

// features/03_inventory/store/inventory.store.ts
import { defineStore } from 'pinia'
import {
  getInventoryList,
  getInventoryById,
  createInventory,
  updateInventory,
  deleteInventory,
} from '../api'
import type { Inventory, InventoryQueryParams, PaginatedResponse } from '../types'

export const useInventoryStore = defineStore('inventory', () => {
  const items = ref<Inventory[]>([])
  const currentItem = ref<Inventory | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)
  const meta = ref({
    total: 0,
    page: 1,
    limit: 20,
    totalPages: 0,
  })

  // Fetch list
  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'
      throw e
    } finally {
      loading.value = false
    }
  }

  // Fetch single
  const fetchItem = async (id: string) => {
    loading.value = true
    error.value = null

    try {
      currentItem.value = await getInventoryById(id)
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Failed to fetch'
      throw e
    } finally {
      loading.value = false
    }
  }

  // Create
  const addItem = async (data: CreateInventoryDto) => {
    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'
      throw e
    } finally {
      loading.value = false
    }
  }

  // Update
  const editItem = async (id: string, data: UpdateInventoryDto) => {
    try {
      const updated = await updateInventory(id, data)
      const index = items.value.findIndex(item => item.id === id)
      if (index !== -1) {
        items.value[index] = updated
      }
      return updated
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Failed to update'
      throw e
    }
  }

  // Delete
  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'
      throw e
    }
  }

  return {
    items,
    currentItem,
    loading,
    error,
    meta,
    fetchItems,
    fetchItem,
    addItem,
    editItem,
    removeItem,
  }
})

Best Practices

api/
├── getList.ts    # GET /inventory
├── getById.ts    # GET /inventory/:id
├── create.ts     # POST /inventory
├── update.ts     # PUT /inventory/:id
└── delete.ts     # DELETE /inventory/:id
  • Define types for request params
  • Define types for request body (DTOs)
  • Define types for response
  • Export types from feature module
// features/03_inventory/index.ts
export * from './api'
export * from './types'
export * from './store'

// Usage
import { getInventoryList, Inventory } from '~/features/03_inventory'
  • API layer: Transform HTTP errors to meaningful messages
  • Store: Store error state for UI
  • Composable: Show toast/notification
  • Component: Display error UI

Next Steps

Data Flow

Complete data flow patterns

State Management

Pinia store patterns