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 & Auth Interceptors
MStore Dashboard menggunakan Nuxt’s built-in $fetch dengan custom interceptors untuk menangani JWT authentication, auto token refresh, dan error handling.
Security Architecture
Token Storage Strategy
Token Storage Lifetime Security Access Token Memory (Pinia) 15-30 min Not accessible by XSS Refresh Token HttpOnly Cookie 7-30 days Not accessible by JS
Auth Plugin Implementation
// plugins/auth.client.ts
export default defineNuxtPlugin (() => {
const authStore = useAuthStore ()
const config = useRuntimeConfig ()
// Track ongoing refresh to prevent multiple simultaneous refreshes
let refreshPromise : Promise < string > | null = null
// Custom fetch with auth
const apiFetch = $fetch . create ({
baseURL: config . public . apiBase ,
// Add auth header to every request
onRequest ({ options }) {
const token = authStore . accessToken
if ( token ) {
options . headers = {
... options . headers ,
Authorization: `Bearer ${ token } ` ,
}
}
// Include cookies for refresh token
options . credentials = 'include'
},
// Handle response errors
async onResponseError ({ response , options }) {
// Only handle 401 errors
if ( response . status !== 401 ) {
throw createError ({
statusCode: response . status ,
message: response . _data ?. message || 'Request failed' ,
})
}
// Prevent infinite loop - don't retry refresh endpoint
if ( options . url ?. includes ( '/auth/refresh' )) {
authStore . logout ()
throw createError ({
statusCode: 401 ,
message: 'Session expired. Please login again.' ,
})
}
// Try to refresh the token
try {
const newToken = await refreshToken ()
// Retry the original request with new token
return $fetch ( response . url , {
... options ,
headers: {
... options . headers ,
Authorization: `Bearer ${ newToken } ` ,
},
})
} catch ( error ) {
authStore . logout ()
navigateTo ( '/login' )
throw error
}
},
})
// Refresh token function with deduplication
const refreshToken = async () : Promise < string > => {
// If refresh is already in progress, wait for it
if ( refreshPromise ) {
return refreshPromise
}
// Start new refresh
refreshPromise = ( async () => {
try {
const response = await $fetch <{ access_token : string }>(
` ${ config . public . apiBase } /api/v1/auth/refresh-token` ,
{
method: 'POST' ,
credentials: 'include' , // Send HttpOnly cookie
}
)
authStore . setToken ( response . access_token )
return response . access_token
} finally {
refreshPromise = null
}
})()
return refreshPromise
}
// Provide custom fetch globally
return {
provide: {
api: apiFetch ,
},
}
} )
Using the API Client
In Composables
// features/03_inventory/api/getList.ts
export const getInventoryList = async ( params ?: InventoryQueryParams ) => {
const { $api } = useNuxtApp ()
return await $api < InventoryResponse >( '/api/v1/inventory' , {
method: 'GET' ,
query: params ,
})
}
In Components
< script setup lang = "ts" >
const { $api } = useNuxtApp ()
const fetchData = async () => {
const data = await $api ( '/api/v1/inventory' )
console . log ( data )
}
</ script >
Direct $fetch (without auth)
Untuk public endpoints yang tidak memerlukan authentication:
// Public API call
const publicData = await $fetch ( '/api/public/health' )
Token Refresh Flow
Error Handling
API Error Types
// types/api.types.ts
interface ApiError {
statusCode : number
message : string
errors ?: Record < string , string []>
}
interface ValidationError extends ApiError {
statusCode : 422
errors : Record < string , string []>
}
Handling Errors in Composables
// features/03_inventory/composables/useInventoryList.ts
export const useInventoryList = () => {
const store = useInventoryStore ()
const toast = useToast ()
const fetchItems = async () => {
try {
await store . fetchItems ()
} catch ( error ) {
if ( error instanceof FetchError ) {
switch ( error . statusCode ) {
case 401 :
// Already handled by plugin (redirect to login)
break
case 403 :
toast . add ({
title: 'Access Denied' ,
description: 'You do not have permission to view inventory.' ,
color: 'red' ,
})
break
case 422 :
// Validation error
const errors = error . data ?. errors
// Handle validation errors
break
case 500 :
toast . add ({
title: 'Server Error' ,
description: 'Please try again later.' ,
color: 'red' ,
})
break
default :
toast . add ({
title: 'Error' ,
description: error . message || 'Something went wrong.' ,
color: 'red' ,
})
}
}
}
}
return { fetchItems }
}
Request Interceptor Options
const response = await $api ( '/api/v1/inventory' , {
headers: {
'X-Custom-Header' : 'value' ,
},
})
With Query Parameters
const response = await $api ( '/api/v1/inventory' , {
query: {
page: 1 ,
limit: 20 ,
search: 'laptop' ,
},
})
POST with Body
const response = await $api ( '/api/v1/inventory' , {
method: 'POST' ,
body: {
name: 'New Product' ,
price: 100000 ,
},
})
File Upload
const formData = new FormData ()
formData . append ( 'file' , file )
const response = await $api ( '/api/v1/upload' , {
method: 'POST' ,
body: formData ,
})
Timeout and Retry
// With timeout
const response = await $api ( '/api/v1/slow-endpoint' , {
timeout: 30000 , // 30 seconds
})
// With retry
const response = await $api ( '/api/v1/unstable-endpoint' , {
retry: 3 ,
retryDelay: 1000 , // 1 second between retries
})
Response Type Hints
// Type-safe response
interface Product {
id : string
name : string
price : number
}
const product = await $api < Product >( '/api/v1/products/123' )
// product is typed as Product
// Array response
const products = await $api < Product []>( '/api/v1/products' )
// products is typed as Product[]
// Paginated response
interface PaginatedResponse < T > {
data : T []
meta : {
total : number
page : number
limit : number
}
}
const response = await $api < PaginatedResponse < Product >>( '/api/v1/products' )
// response.data is Product[], response.meta is typed
Multiple Simultaneous Requests
Saat ada multiple request bersamaan dan token expired:
// Multiple requests at once
const [ products , categories , suppliers ] = await Promise . all ([
$api ( '/api/v1/products' ),
$api ( '/api/v1/categories' ),
$api ( '/api/v1/suppliers' ),
])
// If token expires during these requests:
// 1. First request gets 401
// 2. Plugin starts refresh
// 3. Other requests wait for refresh
// 4. All requests retry with new token
Ini ditangani oleh refreshPromise deduplication:
let refreshPromise : Promise < string > | null = null
const refreshToken = async () : Promise < string > => {
// If refresh is already in progress, return existing promise
if ( refreshPromise ) {
return refreshPromise
}
refreshPromise = ( async () => {
// ... refresh logic
})()
return refreshPromise
}
Best Practices
Always Use $api for Auth Routes
// Good - uses auth interceptor
const data = await $api ( '/api/v1/inventory' )
// Bad - no auth header
const data = await $fetch ( '/api/v1/inventory' )
Handle Errors at Composable Level
// Good - error handling in composable
const fetchItems = async () => {
try {
await store . fetchItems ()
} catch ( error ) {
handleError ( error )
}
}
// Avoid - error handling in component
// Component should just call composable
// Good - typed response
const product = await $api < Product >( '/api/v1/products/123' )
// Avoid - untyped response
const product = await $api ( '/api/v1/products/123' )
// Required for HttpOnly cookies
options . credentials = 'include'
Next Steps
API Layer Pattern Feature API organization
Authentication Complete auth implementation