API Layer Pattern
Setiap feature module memiliki API layer yang terorganisir untuk menangani komunikasi HTTP dengan backend.API Layer Structure
Copy
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)
Copy
// 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)
Copy
// 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)
Copy
// 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)
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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,
})
}
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
One File Per Endpoint
One File Per Endpoint
Copy
api/
├── getList.ts # GET /inventory
├── getById.ts # GET /inventory/:id
├── create.ts # POST /inventory
├── update.ts # PUT /inventory/:id
└── delete.ts # DELETE /inventory/:id
Type Everything
Type Everything
- Define types for request params
- Define types for request body (DTOs)
- Define types for response
- Export types from feature module
Use Barrel Exports
Use Barrel Exports
Copy
// features/03_inventory/index.ts
export * from './api'
export * from './types'
export * from './store'
// Usage
import { getInventoryList, Inventory } from '~/features/03_inventory'
Handle Errors Appropriately
Handle Errors Appropriately
- API layer: Transform HTTP errors to meaningful messages
- Store: Store error state for UI
- Composable: Show toast/notification
- Component: Display error UI