Skip to main content

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