Skip to main content

@54vie/api - API Client

Package for managing networking, API calls, caching, and real-time connections.

Installation

pnpm add @54vie/api

Quick Start

import { apiClient, useQuery, useMutation } from '@54vie/api';

// Simple GET request
const users = await apiClient.get('/users');

// With React hook
function UserList() {
const { data, isLoading, error } = useQuery({
key: ['users'],
fn: () => apiClient.get('/users'),
});

if (isLoading) return <Loading />;
return <FlatList data={data} />;
}

API Client

Basic Requests

import { apiClient } from '@54vie/api';

// GET
const users = await apiClient.get('/users');
const user = await apiClient.get('/users/123');

// POST
const newUser = await apiClient.post('/users', {
name: 'John',
email: 'john@example.com',
});

// PUT
await apiClient.put('/users/123', { name: 'Jane' });

// PATCH
await apiClient.patch('/users/123', { status: 'active' });

// DELETE
await apiClient.delete('/users/123');

Request Options

const data = await apiClient.get('/products', {
params: { category: 'electronics', page: 1 },
headers: { 'X-Custom-Header': 'value' },
timeout: 10000,
cache: true,
cacheTTL: 5 * 60 * 1000, // 5 minutes
});

File Upload

const formData = new FormData();
formData.append('file', {
uri: fileUri,
type: 'image/jpeg',
name: 'photo.jpg',
});

await apiClient.upload('/upload', formData, {
onProgress: (progress) => {
console.log(`Upload: ${progress.percent}%`);
},
});

React Hooks

useQuery

Fetch data with caching and refetch:

import { useQuery } from '@54vie/api';

function ProductList() {
const {
data,
isLoading,
error,
refetch,
isRefetching,
} = useQuery({
key: ['products', categoryId],
fn: () => apiClient.get(`/products?category=${categoryId}`),
staleTime: 5 * 60 * 1000, // Cache 5 minutes
refetchOnMount: true,
refetchOnFocus: true,
onSuccess: (data) => console.log('Loaded:', data),
onError: (error) => console.error('Error:', error),
});

return (
<FlatList
data={data}
refreshing={isRefetching}
onRefresh={refetch}
/>
);
}

useMutation

For operations that change data (POST, PUT, DELETE):

import { useMutation, invalidateQueries } from '@54vie/api';

function CreateProduct() {
const { mutate, isLoading, error } = useMutation({
fn: (data) => apiClient.post('/products', data),
onSuccess: (result) => {
Toast.success('Product created!');
invalidateQueries(['products']); // Refetch product list
},
onError: (error) => {
Toast.error(error.message);
},
});

const handleSubmit = () => {
mutate({ name, price, category });
};

return (
<Button onPress={handleSubmit} loading={isLoading}>
Create
</Button>
);
}

useInfiniteQuery

For pagination/infinite scroll:

import { useInfiniteQuery } from '@54vie/api';

function InfiniteList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
key: ['posts'],
fn: ({ pageParam = 1 }) =>
apiClient.get(`/posts?page=${pageParam}`),
getNextPageParam: (lastPage) => lastPage.nextPage,
});

// Flatten pages
const posts = data?.pages.flatMap(page => page.items) ?? [];

return (
<FlatList
data={posts}
onEndReached={() => hasNextPage && fetchNextPage()}
ListFooterComponent={
isFetchingNextPage ? <Loading /> : null
}
/>
);
}

Interceptors

Request Interceptor

import { apiClient } from '@54vie/api';

// Add auth token to all requests
apiClient.interceptors.request.use((config) => {
const token = TokenManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

Response Interceptor

// Handle 401 errors globally
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Try refresh token
const refreshed = await TokenManager.refreshToken();
if (refreshed) {
// Retry original request
return apiClient.request(error.config);
}
// Redirect to login
navigation.navigate('Login');
}
throw error;
}
);

Caching

Cache Manager

import { ApiCacheManager } from '@54vie/api';

// Set cache
ApiCacheManager.set('user:123', userData, 5 * 60 * 1000);

// Get cache
const cached = ApiCacheManager.get('user:123');

// Invalidate
ApiCacheManager.invalidate('user:123');
ApiCacheManager.invalidateByPrefix('user:');

// Clear all
ApiCacheManager.clear();

Cache Strategies

import {
cacheFirst,
networkFirst,
staleWhileRevalidate,
} from '@54vie/api';

// Cache first - Use cache if available
const data = await cacheFirst('products', () =>
apiClient.get('/products')
);

// Network first - Always try network, fallback to cache
const data = await networkFirst('products', () =>
apiClient.get('/products')
);

// Stale while revalidate - Return cache immediately, update in background
const data = await staleWhileRevalidate('products', fetcher, {
onRevalidate: (newData) => {
console.log('Data updated:', newData);
},
});

WebSocket

Basic Usage

import { wsClient } from '@54vie/api';

// Connect
await wsClient.connect('wss://api.54vie.vn/ws');

// Subscribe to channel
wsClient.subscribe('chat:room-123', (message) => {
console.log('New message:', message);
});

// Send message
wsClient.send('chat:room-123', { text: 'Hello!' });

// Unsubscribe
wsClient.unsubscribe('chat:room-123');

// Disconnect
wsClient.disconnect();

useWebSocket Hook

import { useWebSocket, useChannel } from '@54vie/api';

function ChatRoom({ roomId }) {
const { isConnected, send } = useWebSocket();

const { messages } = useChannel(`chat:${roomId}`, {
onMessage: (msg) => setMessages(prev => [...prev, msg]),
});

const sendMessage = (text) => {
send(`chat:${roomId}`, { text, userId: currentUser.id });
};

return (
<View>
<FlatList data={messages} />
<MessageInput onSend={sendMessage} />
</View>
);
}

Offline Support

Offline Queue

import { useOffline, offlineQueue } from '@54vie/api';

function CreateOrder() {
const { isOnline } = useOffline();

const handleSubmit = async () => {
if (isOnline) {
await apiClient.post('/orders', orderData);
} else {
// Queue for later
offlineQueue.add({
method: 'POST',
url: '/orders',
data: orderData,
});
Toast.info('Order saved. Will sync when online.');
}
};
}

Auto Sync

import { offlineQueue } from '@54vie/api';

// Process queue when online
offlineQueue.process();

// Listen to queue changes
offlineQueue.onStatusChange((status) => {
console.log(`Queue: ${status.pending} pending, ${status.failed} failed`);
});

Error Handling

import {
ApiError,
getErrorMessage,
isRetryableError,
isAuthError,
} from '@54vie/api';

try {
await apiClient.post('/orders', data);
} catch (error) {
if (error instanceof ApiError) {
// Get localized message
const message = getErrorMessage(error.code);

if (isAuthError(error.code)) {
// Handle auth error
navigation.navigate('Login');
} else if (isRetryableError(error.code)) {
// Can retry
Toast.error(message + ' Tap to retry.');
} else {
Toast.error(message);
}
}
}

Configuration

import { configureApi } from '@54vie/api';

configureApi({
baseURL: 'https://api.54vie.vn/v1',
timeout: 30000,
headers: {
'X-App-Version': '1.0.0',
'X-Platform': Platform.OS,
},
retry: {
attempts: 3,
delay: 1000,
statusCodes: [408, 500, 502, 503, 504],
},
cache: {
enabled: true,
ttl: 5 * 60 * 1000,
},
});