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.
Development Guide
Panduan best practices untuk development di MStore Dashboard.
Development Workflow
Starting Development
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Dashboard runs at http://localhost:4001
Code Generation
Saat membuat perubahan pada types atau schema:
# Generate types (if using code-gen)
pnpm generate:types
Project Structure Best Practices
Feature Module Structure
Setiap feature module harus mengikuti struktur ini:
features/{domain}/
├── api/ # API calls
│ ├── index.ts # Barrel export
│ ├── getList.ts
│ ├── create.ts
│ └── ...
├── components/ # Domain-specific components
│ ├── index.ts
│ └── {Domain}Table.vue
├── composables/ # Business logic
│ ├── index.ts
│ └── use{Domain}List.ts
├── store/ # Pinia store
│ ├── index.ts
│ └── {domain}.store.ts
├── types/ # TypeScript types
│ ├── index.ts
│ └── {domain}.types.ts
└── index.ts # Main barrel export
Barrel Exports
Selalu gunakan barrel exports untuk clean imports:
// features/03_inventory/index.ts
export * from './api'
export * from './store'
export * from './composables'
export * from './types'
// Export components dengan named export
export { default as InventoryTable } from './components/InventoryTable.vue'
Usage:
import {
useInventoryStore ,
useInventoryList ,
InventoryTable ,
type Inventory ,
} from '~/features/03_inventory'
Naming Conventions
Files
Type Convention Example Page kebab-caseinventory-list.vueComponent PascalCaseInventoryTable.vueComposable camelCaseuseInventoryList.tsStore kebab-caseinventory.store.tsType kebab-caseinventory.types.tsAPI camelCasegetList.ts
Code
Type Convention Example Component PascalCaseInventoryTableComposable usePascalCaseuseInventoryListStore usePascalCaseStoreuseInventoryStoreFunction camelCasegetInventoryListInterface PascalCaseInventoryType PascalCaseInventoryStatusConstant SCREAMING_SNAKE_CASEMAX_ITEMS
TypeScript Best Practices
Always Type Everything
// Good
interface User {
id : string
name : string
email : string
}
const fetchUser = async ( id : string ) : Promise < User > => {
return await $api < User >( `/users/ ${ id } ` )
}
// Avoid
const fetchUser = async ( id ) => {
return await $api ( `/users/ ${ id } ` )
}
Use Strict Mode
// tsconfig.json
{
"compilerOptions" : {
"strict" : true ,
"noImplicitAny" : true ,
"strictNullChecks" : true
}
}
Prefer Interfaces Over Types
// Prefer for objects
interface User {
id : string
name : string
}
// Use type for unions, intersections
type Status = 'active' | 'inactive'
type UserWithRole = User & { role : string }
Vue 3 Best Practices
Composition API
Always use Composition API with <script setup>:
< script setup lang = "ts" >
import { useInventoryStore } from '~/features/03_inventory'
const store = useInventoryStore ()
// Reactive state
const searchQuery = ref ( '' )
// Computed
const filteredItems = computed (() =>
store . items . filter ( item =>
item . name . includes ( searchQuery . value )
)
)
// Methods
const handleSearch = ( query : string ) => {
searchQuery . value = query
}
</ script >
Props & Emits Typing
< script setup lang = "ts" >
interface Props {
items : Product []
loading ?: boolean
}
const props = withDefaults ( defineProps < Props >(), {
loading: false ,
})
const emit = defineEmits <{
select : [ item : Product ]
delete : [ id : string ]
}>()
</ script >
Use v-model Properly
<!-- Parent -->
< template >
< CustomInput v-model = " value " />
</ template >
<!-- CustomInput.vue -->
< script setup lang = "ts" >
const model = defineModel < string >()
</ script >
< template >
< input : value = " model " @ input = " model = $event . target . value " />
</ template >
State Management Best Practices
Store Organization
// features/03_inventory/store/inventory.store.ts
export const useInventoryStore = defineStore ( 'inventory' , () => {
// 1. State
const items = ref < Inventory []>([])
const loading = ref ( false )
const error = ref < string | null >( null )
// 2. Getters (computed)
const hasItems = computed (() => items . value . length > 0 )
// 3. Actions
const fetchItems = async () => {
loading . value = true
try {
items . value = await getInventoryList ()
} catch ( e ) {
error . value = e . message
} finally {
loading . value = false
}
}
// 4. Reset
const $reset = () => {
items . value = []
loading . value = false
error . value = null
}
return {
items ,
loading ,
error ,
hasItems ,
fetchItems ,
$reset ,
}
})
Composable vs Store
Use Case Use Store Use Composable Shared across pages ✅ ❌ Single page only ❌ ✅ Need persistence ✅ ❌ Form state ❌ ✅ UI state ❌ ✅ (or ref)
API Best Practices
Error Handling
// API layer - transform errors
export const getInventoryList = async () => {
try {
return await $api < Inventory []>( '/api/v1/inventory' )
} catch ( error ) {
if ( error . statusCode === 404 ) {
throw new Error ( 'Inventory not found' )
}
throw error
}
}
// Store - store error state
const fetchItems = async () => {
try {
items . value = await getInventoryList ()
} catch ( e ) {
error . value = e . message
}
}
// Composable - show notification
const { fetchItems } = useInventoryList ()
const handleFetch = async () => {
try {
await fetchItems ()
} catch {
toast . add ({
title: 'Error' ,
description: 'Failed to load inventory' ,
color: 'error' ,
})
}
}
Component Best Practices
Single Responsibility
<!-- Good: Single responsibility -->
< ProductTable : items = " items " @ select = " handleSelect " />
< ProductForm : product = " selected " @ save = " handleSave " />
<!-- Avoid: Too many responsibilities -->
< ProductManager /> <!-- Does everything -->
Props Down, Events Up
<!-- Parent -->
< template >
< ProductList
: items = " items "
: loading = " loading "
@ refresh = " fetchItems "
@ delete = " handleDelete "
/>
</ template >
<!-- ProductList.vue -->
< script setup lang = "ts" >
defineProps <{
items : Product []
loading : boolean
}>()
const emit = defineEmits <{
refresh : []
delete : [ id : string ]
}>()
</ script >
Lazy Loading
// Lazy load routes
// Nuxt does this automatically with file-based routing
// Lazy load components
const HeavyComponent = defineAsyncComponent (() =>
import ( './HeavyComponent.vue' )
)
Untuk list besar, gunakan virtual scrolling:
< template >
< VirtualScroller
: items = " items "
: item-height = " 50 "
class = "h-[500px]"
>
< template # default = " { item } " >
< ProductRow : product = " item " />
</ template >
</ VirtualScroller >
</ template >
Computed Caching
// Good: Computed caches results
const filteredItems = computed (() =>
items . value . filter ( item => item . active )
)
// Avoid: Re-computes every render
const getFilteredItems = () =>
items . value . filter ( item => item . active )
Debugging
Install Vue DevTools browser extension
Inspect component hierarchy
Debug Pinia stores
Track events
Vue Inspector
// nuxt.config.ts
import VueInspector from 'vite-plugin-vue-inspector'
export default defineNuxtConfig ({
vite: {
plugins: [
VueInspector ({
enabled: true ,
toggleButtonVisibility: 'always' ,
}),
],
} ,
})
Click the inspector button to jump to component source code.
Console Logging
// Use structured logging
console . log ( '[Inventory]' , 'Fetching items...' , { page , limit })
// Or use a logger utility
import { logger } from '~/utils/logger'
logger . info ( 'Fetching items' , { page , limit })
Common Patterns
Loading State Pattern
< script setup lang = "ts" >
const { data , loading , error , refresh } = useAsyncData (
'inventory' ,
() => getInventoryList ()
)
</ script >
< template >
< USkeleton v-if = " loading " />
< UAlert v-else-if = " error " color = "error" : title = " error . message " />
< InventoryTable v-else : items = " data " />
</ template >
< script setup lang = "ts" >
const form = reactive ({
name: '' ,
email: '' ,
})
const errors = reactive ({
name: '' ,
email: '' ,
})
const isSubmitting = ref ( false )
const validate = () => {
errors . name = form . name ? '' : 'Required'
errors . email = form . email ? '' : 'Required'
return ! errors . name && ! errors . email
}
const handleSubmit = async () => {
if ( ! validate ()) return
isSubmitting . value = true
try {
await submitForm ( form )
toast . add ({ title: 'Saved!' })
} finally {
isSubmitting . value = false
}
}
</ script >
Checklist for New Features
Next Steps
Testing Strategy Testing best practices
Architecture DDD architecture guide