API Integration Patterns
Overview
The MyTradeX platform implements a comprehensive RESTful API architecture with multiple integration patterns to support various client applications and external systems. This document outlines the API design principles, integration patterns, and best practices used throughout the platform.
API Architecture Principles
Core Design Principles
- RESTful Design: RESTful endpoints following HTTP semantics
- Resource-Based URLs: Clear, hierarchical resource naming
- Stateless Communication: Each request contains all necessary information
- Consistent Response Format: Standardized API response structure
- Error Handling: Comprehensive error codes and messages
- Versioning: API versioning for backward compatibility
- Documentation: OpenAPI/Swagger specification for all endpoints
- Security: JWT-based authentication and authorization
API Design Standards
Resource Naming Conventions:
// Resource-based URLs
GET /api/v1/users // Get all users
GET /api/v1/users/{id} // Get specific user
POST /api/v1/users // Create new user
PUT /api/v1/users/{id} // Update user
DELETE /api/v1/users/{id} // Delete user
// Trading-specific resources
GET /api/v1/orders // Get orders
POST /api/v1/orders // Create order
GET /api/v1/orders/{id} // Get specific order
PUT /api/v1/orders/{id} // Update order
DELETE /api/v1/orders/{id} // Cancel order
// Market data resources
GET /api/v1/market-data/{symbol} // Get market data
GET /api/v1/orderbook/{symbol} // Get order book
GET /api/v1/trades/{symbol} // Get trade history
Response Format Standard:
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
details?: any;
};
pagination?: {
page: number;
size: number;
total: number;
totalPages: number;
};
timestamp: string;
requestId: string;
}
// Success response
{
"success": true,
"data": {
"id": "123",
"symbol": "AAPL",
"quantity": 100,
"price": 150.00
},
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "req-abc-123"
}
// Error response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid order parameters",
"details": {
"field": "quantity",
"reason": "must be positive"
}
},
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "req-abc-123"
}
Core API Endpoints
1. Authentication API
Base URL: /api/v1/auth
Login Endpoint
POST /api/v1/auth/login
Content-Type: application/json
Request:
{
"email": "user@example.com",
"password": "password123"
}
Response:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "refresh-token-here",
"user": {
"id": "user-123",
"email": "user@example.com",
"name": "John Doe",
"role": "TRADER"
},
"expiresIn": 3600
}
}
Refresh Token Endpoint
POST /api/v1/auth/refresh
Content-Type: application/json
Request:
{
"refreshToken": "refresh-token-here"
}
Response:
{
"success": true,
"data": {
"token": "new-jwt-token-here",
"expiresIn": 3600
}
}
Logout Endpoint
POST /api/v1/auth/logout
Authorization: Bearer {token}
Response:
{
"success": true,
"message": "Successfully logged out"
}
Current User Endpoint
GET /api/v1/auth/me
Authorization: Bearer {token}
Response:
{
"success": true,
"data": {
"id": "user-123",
"email": "user@example.com",
"name": "John Doe",
"role": "TRADER",
"permissions": ["READ_MARKET_DATA", "CREATE_ORDERS"]
}
}
2. User Management API
Base URL: /api/v1/users
Get All Users (Admin Only)
GET /api/v1/users?page=1&size=20&role=TRADER&status=ACTIVE
Authorization: Bearer {token}
Response:
{
"success": true,
"data": [
{
"id": "user-123",
"email": "user@example.com",
"name": "John Doe",
"role": "TRADER",
"status": "ACTIVE",
"createdAt": "2024-01-01T00:00:00Z",
"lastLogin": "2024-01-15T09:30:00Z"
}
],
"pagination": {
"page": 1,
"size": 20,
"total": 100,
"totalPages": 5
}
}
Create User (Admin Only)
POST /api/v1/users
Authorization: Bearer {adminToken}
Request:
{
"email": "newuser@example.com",
"name": "Jane Doe",
"role": "TRADER",
"password": "tempPassword123"
}
Response:
{
"success": true,
"data": {
"id": "user-456",
"email": "newuser@example.com",
"name": "Jane Doe",
"role": "TRADER",
"status": "ACTIVE",
"createdAt": "2024-01-15T10:30:00Z"
}
}
Update User (Admin Only)
PUT /api/v1/users/{userId}
Authorization: Bearer {adminToken}
Request:
{
"name": "Jane Smith",
"role": "ADMIN",
"status": "SUSPENDED"
}
Response:
{
"success": true,
"data": {
"id": "user-456",
"email": "newuser@example.com",
"name": "Jane Smith",
"role": "ADMIN",
"status": "SUSPENDED",
"updatedAt": "2024-01-15T11:00:00Z"
}
}
3. Order Management API
Base URL: /api/v1/orders
Create Order
POST /api/v1/orders
Authorization: Bearer {token}
Request:
{
"symbol": "AAPL",
"side": "BUY",
"orderType": "LIMIT",
"quantity": 100,
"price": 150.00,
"timeInForce": "DAY"
}
Response:
{
"success": true,
"data": {
"id": "order-123",
"userId": "user-123",
"symbol": "AAPL",
"side": "BUY",
"orderType": "LIMIT",
"quantity": 100,
"price": 150.00,
"remainingQuantity": 100,
"status": "NEW",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}
Get User Orders
GET /api/v1/orders?status=NEW&symbol=AAPL&page=1&size=20
Authorization: Bearer {token}
Response:
{
"success": true,
"data": [
{
"id": "order-123",
"symbol": "AAPL",
"side": "BUY",
"quantity": 100,
"filledQuantity": 50,
"remainingQuantity": 50,
"price": 150.00,
"status": "PARTIAL",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:45:00Z"
}
],
"pagination": {
"page": 1,
"size": 20,
"total": 50,
"totalPages": 3
}
}
Get Specific Order
GET /api/v1/orders/{orderId}
Authorization: Bearer {token}
Response:
{
"success": true,
"data": {
"id": "order-123",
"symbol": "AAPL",
"side": "BUY",
"orderType": "LIMIT",
"quantity": 100,
"price": 150.00,
"filledQuantity": 100,
"remainingQuantity": 0,
"averageFillPrice": 149.50,
"status": "FILLED",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:45:00Z",
"trades": [
{
"id": "trade-456",
"quantity": 50,
"price": 149.50,
"timestamp": "2024-01-15T10:35:00Z"
}
]
}
}
Cancel Order
DELETE /api/v1/orders/{orderId}
Authorization: Bearer {token}
Response:
{
"success": true,
"data": {
"id": "order-123",
"status": "CANCELLED",
"updatedAt": "2024-01-15T11:00:00Z"
}
}
4. Market Data API
Base URL: /api/v1/market-data
Get Market Data for Symbol
GET /api/v1/market-data/AAPL
Authorization: Bearer {token}
Response:
{
"success": true,
"data": {
"symbol": "AAPL",
"lastPrice": 150.25,
"bidPrice": 150.20,
"askPrice": 150.30,
"bidQuantity": 1000,
"askQuantity": 800,
"volume": 1000000,
"high24h": 152.00,
"low24h": 149.50,
"change24h": 2.25,
"changePercent24h": 1.52,
"timestamp": "2024-01-15T10:30:00Z"
}
}
Get Multiple Symbols
POST /api/v1/market-data/batch
Authorization: Bearer {token}
Request:
{
"symbols": ["AAPL", "GOOGL", "MSFT"]
}
Response:
{
"success": true,
"data": [
{
"symbol": "AAPL",
"lastPrice": 150.25,
// ... other fields
},
{
"symbol": "GOOGL",
"lastPrice": 2750.00,
// ... other fields
}
]
}
Get Order Book
GET /api/v1/orderbook/AAPL?depth=10
Authorization: Bearer {token}
Response:
{
"success": true,
"data": {
"symbol": "AAPL",
"timestamp": "2024-01-15T10:30:00Z",
"bids": [
[150.20, 1000],
[150.15, 1500],
[150.10, 2000]
],
"asks": [
[150.30, 800],
[150.35, 1200],
[150.40, 1600]
]
}
}
5. Portfolio API
Base URL: /api/v1/portfolio
Get User Positions
GET /api/v1/portfolio/positions
Authorization: Bearer {token}
Response:
{
"success": true,
"data": [
{
"id": "position-123",
"symbol": "AAPL",
"quantity": 100,
"averageCost": 149.50,
"currentPrice": 150.25,
"unrealizedPnL": 75.00,
"realizedPnL": 125.00,
"lastUpdated": "2024-01-15T10:30:00Z"
}
]
}
Get Portfolio Summary
GET /api/v1/portfolio/summary
Authorization: Bearer {token}
Response:
{
"success": true,
"data": {
"totalValue": 150250.00,
"totalUnrealizedPnL": 150.00,
"totalRealizedPnL": 125.00,
"dayPnL": 25.00,
"positions": [
{
"symbol": "AAPL",
"quantity": 100,
"marketValue": 15025.00,
"pnL": 75.00
}
]
}
}
Integration Patterns
1. Frontend Integration
React Query Hooks Integration
Location: mytradex-monorepo/packages/hooks/src/useOrders.ts
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@mytradex/api';
import type { Order, CreateOrderRequest } from '@mytradex/types';
// Hook for fetching orders
export const useOrders = (filters?: OrderFilters) => {
return useQuery({
queryKey: ['orders', filters],
queryFn: async () => {
const response = await apiClient.get<Order[]>('/orders', {
params: filters
});
return response.data;
},
staleTime: 30000, // 30 seconds
cacheTime: 300000, // 5 minutes
});
};
// Hook for creating orders
export const useCreateOrder = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (orderData: CreateOrderRequest): Promise<Order> => {
const response = await apiClient.post<Order>('/orders', orderData);
return response.data;
},
onSuccess: (newOrder) => {
// Update orders cache
queryClient.setQueryData(['orders'], (old: Order[] | undefined) => {
return old ? [newOrder, ...old] : [newOrder];
});
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: ['portfolio'] });
},
onError: (error) => {
console.error('Failed to create order:', error);
},
});
};
// Hook for canceling orders
export const useCancelOrder = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (orderId: string): Promise<Order> => {
const response = await apiClient.delete<Order>(`/orders/${orderId}`);
return response.data;
},
onSuccess: (cancelledOrder) => {
// Update orders cache
queryClient.setQueryData(['orders'], (old: Order[] | undefined) => {
return old?.map(order =>
order.id === cancelledOrder.id ? cancelledOrder : order
);
});
},
});
};
Custom Hook Integration
Location: mytradex-monorepo/packages/hooks/src/useMarketData.ts
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@mytradex/api';
import { getSocket } from '@mytradex/api';
export const useMarketData = (symbol: string) => {
return useQuery({
queryKey: ['market-data', symbol],
queryFn: async () => {
const response = await apiClient.get(`/market-data/${symbol}`);
return response.data;
},
enabled: !!symbol,
staleTime: 5000, // 5 seconds for market data
refetchInterval: 5000, // Auto-refetch every 5 seconds
});
};
// Hook for real-time market data
export const useMarketDataRealtime = (symbol: string) => {
const [data, setData] = useState(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const socket = getSocket();
const handleConnect = () => {
setIsConnected(true);
socket.emit('subscribe', `market:${symbol}`);
};
const handleDisconnect = () => {
setIsConnected(false);
};
const handleMarketData = (marketData: any) => {
if (marketData.symbol === symbol) {
setData(marketData);
}
};
socket.on('connect', handleConnect);
socket.on('disconnect', handleDisconnect);
socket.on('market:update', handleMarketData);
return () => {
socket.off('connect', handleConnect);
socket.off('disconnect', handleDisconnect);
socket.off('market:update', handleMarketData);
socket.emit('unsubscribe', `market:${symbol}`);
};
}, [symbol]);
return { data, isConnected };
};
2. API Client Configuration
Centralized API Client
Location: mytradex-monorepo/packages/api/src/apiClient.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
export class ApiClient {
private client: AxiosInstance;
private baseURL: string;
constructor(config: ApiConfig) {
this.baseURL = config.baseURL;
this.client = axios.create({
baseURL: config.baseURL,
timeout: config.timeout || 10000,
headers: {
'Content-Type': 'application/json',
...config.headers,
},
});
this.setupInterceptors();
this.setupRetryLogic();
this.setupRateLimiting();
}
private setupInterceptors() {
// Request interceptor
this.client.interceptors.request.use(
(config) => {
const token = this.getAuthToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Add request ID for tracking
config.headers['X-Request-ID'] = this.generateRequestId();
// Add timestamp
config.headers['X-Client-Timestamp'] = Date.now().toString();
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor
this.client.interceptors.response.use(
(response) => {
// Log successful requests in development
if (process.env.NODE_ENV === 'development') {
console.log(`[API] ${response.config.method?.toUpperCase()} ${response.config.url}`, {
status: response.status,
duration: Date.now() - response.config.metadata?.startTime,
});
}
return response;
},
async (error) => {
const originalRequest = error.config;
// Handle 401 - Token refresh
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newToken = await this.refreshAuthToken();
if (newToken) {
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return this.client(originalRequest);
}
} catch (refreshError) {
this.handleAuthFailure();
return Promise.reject(refreshError);
}
}
// Handle rate limiting
if (error.response?.status === 429) {
const retryAfter = parseInt(error.response.headers['retry-after']) || 60;
await this.delay(retryAfter * 1000);
return this.client(originalRequest);
}
return Promise.reject(this.enhanceError(error));
}
);
}
private setupRetryLogic() {
this.client.interceptors.response.use(
(response) => response,
async (error) => {
const config = error.config;
if (!config || config.method === 'GET') {
return Promise.reject(error);
}
if (!config._retry) {
config._retry = true;
const shouldRetry = this.shouldRetry(error);
if (shouldRetry && config._retryCount < this.maxRetries) {
config._retryCount = (config._retryCount || 0) + 1;
const delay = this.calculateBackoffDelay(config._retryCount);
await this.delay(delay);
return this.client(config);
}
}
return Promise.reject(error);
}
);
}
private setupRateLimiting() {
const rateLimiter = new RateLimiter(100, 60000); // 100 requests per minute
this.client.interceptors.request.use(async (config) => {
await rateLimiter.acquire();
return config;
});
}
// HTTP methods
public async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.get<T>(url, config);
}
public async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.post<T>(url, data, config);
}
public async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.put<T>(url, data, config);
}
public async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.patch<T>(url, data, config);
}
public async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.delete<T>(url, config);
}
// Helper methods
private getAuthToken(): string | null {
return localStorage.getItem('authToken');
}
private async refreshAuthToken(): Promise<string | null> {
try {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) return null;
const response = await axios.post(`${this.baseURL}/auth/refresh`, {
refreshToken
});
const { token } = response.data;
localStorage.setItem('authToken', token);
return token;
} catch (error) {
return null;
}
}
private handleAuthFailure() {
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
private shouldRetry(error: any): boolean {
return error.code === 'ECONNABORTED' ||
error.response?.status >= 500 ||
error.response?.status === 429;
}
private calculateBackoffDelay(retryCount: number): number {
return Math.min(1000 * Math.pow(2, retryCount), 30000);
}
private async delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private enhanceError(error: any) {
if (error.response?.data) {
return {
...error,
message: error.response.data.message || error.response.data.error,
code: error.response.data.code,
status: error.response.status,
};
}
return error;
}
}
// Default instance
export const apiClient = new ApiClient({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api',
timeout: 10000,
});
3. Error Handling Patterns
Global Error Handler
class APIErrorHandler {
static handle(error: any): AppError {
const { response, request, message } = error;
// Network errors
if (!response && request) {
return {
type: 'NETWORK_ERROR',
message: 'Network connection failed',
originalError: error,
};
}
// Server errors
if (response?.status >= 500) {
return {
type: 'SERVER_ERROR',
message: 'Server error occurred',
statusCode: response.status,
originalError: error,
};
}
// Client errors
if (response?.status >= 400) {
const errorData = response.data;
switch (response.status) {
case 400:
return {
type: 'VALIDATION_ERROR',
message: errorData.message || 'Invalid request data',
errors: errorData.details,
originalError: error,
};
case 401:
return {
type: 'UNAUTHORIZED',
message: 'Authentication required',
originalError: error,
};
case 403:
return {
type: 'FORBIDDEN',
message: 'Access denied',
originalError: error,
};
case 404:
return {
type: 'NOT_FOUND',
message: 'Resource not found',
originalError: error,
};
case 429:
return {
type: 'RATE_LIMITED',
message: 'Too many requests',
retryAfter: parseInt(response.headers['retry-after']),
originalError: error,
};
default:
return {
type: 'API_ERROR',
message: errorData.message || 'API request failed',
statusCode: response.status,
originalError: error,
};
}
}
// Generic error
return {
type: 'UNKNOWN_ERROR',
message: message || 'An unexpected error occurred',
originalError: error,
};
}
static handleAsyncError(error: any): never {
const appError = this.handle(error);
throw appError;
}
}
// Error boundary integration
export const withErrorBoundary = <T extends ComponentType<any>>(
Component: T,
fallback?: ComponentType<{ error: AppError }>
): T => {
return ((props: any) => (
<ErrorBoundary fallback={fallback}>
<Component {...props} />
</ErrorBoundary>
)) as T;
};
4. Pagination Patterns
Cursor-based Pagination
interface CursorPaginationParams {
cursor?: string;
limit?: number;
}
interface CursorPaginatedResponse<T> {
data: T[];
pagination: {
nextCursor?: string;
prevCursor?: string;
hasMore: boolean;
total?: number;
};
}
export const useCursorPaginatedQuery = <T>(
key: string[],
fetchFn: (params: CursorPaginationParams) => Promise<CursorPaginatedResponse<T>>,
params: CursorPaginationParams = {}
) => {
return useQuery({
queryKey: [...key, params],
queryFn: () => fetchFn(params),
keepPreviousData: true,
});
};
// Example usage with orders
export const useOrdersWithCursor = (params: CursorPaginationParams = {}) => {
return useCursorPaginatedQuery(
['orders', 'cursor'],
async ({ cursor, limit = 20 }) => {
const response = await apiClient.get('/orders', {
params: { cursor, limit }
});
return response.data;
},
params
);
};
// Infinite scroll component
export const InfiniteOrderList = () => {
const [cursor, setCursor] = useState<string | undefined>();
const {
data,
isLoading,
hasNextPage,
fetchNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['orders', 'infinite'],
queryFn: ({ pageParam }) => apiClient.get('/orders', {
params: { cursor: pageParam, limit: 20 }
}).then(res => res.data),
getNextPageParam: (lastPage) => lastPage.pagination.nextCursor,
initialPageParam: undefined,
});
const orders = data?.pages.flatMap(page => page.data) || [];
return (
<div>
{orders.map(order => <OrderItem key={order.id} order={order} />)}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
};
5. Caching Strategies
Multi-level Caching
class CacheManager {
private memoryCache = new Map<string, CacheEntry>();
private readonly MEMORY_TTL = 5 * 60 * 1000; // 5 minutes
private readonly MAX_MEMORY_ENTRIES = 1000;
set(key: string, data: any, ttl?: number): void {
const entry: CacheEntry = {
data,
timestamp: Date.now(),
ttl: ttl || this.MEMORY_TTL
};
// Memory management
if (this.memoryCache.size >= this.MAX_MEMORY_ENTRIES) {
const firstKey = this.memoryCache.keys().next().value;
this.memoryCache.delete(firstKey);
}
this.memoryCache.set(key, entry);
// Persist to IndexedDB for longer storage
this.persistToStorage(key, data, ttl);
}
get(key: string): any | null {
// Check memory cache first
const memoryEntry = this.memoryCache.get(key);
if (memoryEntry && this.isValid(memoryEntry)) {
return memoryEntry.data;
}
// Remove expired memory entry
if (memoryEntry) {
this.memoryCache.delete(key);
}
// Check persisted storage
return this.getFromStorage(key);
}
invalidate(key: string): void {
this.memoryCache.delete(key);
this.removeFromStorage(key);
}
invalidatePattern(pattern: string): void {
// Invalidate memory cache
const regex = new RegExp(pattern);
for (const cacheKey of this.memoryCache.keys()) {
if (regex.test(cacheKey)) {
this.memoryCache.delete(cacheKey);
}
}
// Invalidate persisted storage
this.invalidateStoragePattern(pattern);
}
private isValid(entry: CacheEntry): boolean {
return Date.now() - entry.timestamp < entry.ttl;
}
private async persistToStorage(key: string, data: any, ttl?: number): Promise<void> {
try {
const cacheData = {
data,
timestamp: Date.now(),
ttl: ttl || this.MEMORY_TTL
};
await indexedDB.set(key, cacheData);
} catch (error) {
console.warn('Failed to persist cache to storage:', error);
}
}
private async getFromStorage(key: string): Promise<any | null> {
try {
const cacheData = await indexedDB.get(key);
if (cacheData && this.isValid(cacheData)) {
// Restore to memory cache
this.memoryCache.set(key, cacheData);
return cacheData.data;
}
} catch (error) {
console.warn('Failed to retrieve cache from storage:', error);
}
return null;
}
private async removeFromStorage(key: string): Promise<void> {
try {
await indexedDB.delete(key);
} catch (error) {
console.warn('Failed to remove cache from storage:', error);
}
}
private async invalidateStoragePattern(pattern: string): Promise<void> {
try {
// Implementation for clearing IndexedDB entries matching pattern
const keys = await indexedDB.getAllKeys();
const regex = new RegExp(pattern);
for (const key of keys) {
if (regex.test(key as string)) {
await indexedDB.delete(key as string);
}
}
} catch (error) {
console.warn('Failed to invalidate storage pattern:', error);
}
}
}
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
}
// Global cache instance
export const cacheManager = new CacheManager();
Smart Cache Invalidation
class SmartCacheInvalidator {
private static readonly DEPENDENCIES = {
'orders': ['portfolio', 'positions', 'market-data'],
'market-data': ['positions', 'orderbook'],
'portfolio': ['positions'],
};
static invalidateDependencies(changedResource: string): void {
const dependencies = this.DEPENDENCIES[changedResource];
if (dependencies) {
dependencies.forEach(dep => {
this.invalidateByPattern(`${dep}:*`);
});
}
// Always invalidate the resource itself
this.invalidateByPattern(`${changedResource}:*`);
}
private static invalidateByPattern(pattern: string): void {
cacheManager.invalidatePattern(pattern);
queryClient.invalidateQueries({ queryKey: [pattern.split(':')[0]] });
}
// Optimistic updates with cache update
static optimisticUpdate<T>(
queryKey: string[],
updateFn: (oldData: T) => T,
mutationFn: () => Promise<T>
): Promise<T> {
return queryClient.executeMutation({
mutationFn,
onMutate: async () => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey });
// Snapshot previous value
const previousData = queryClient.getQueryData<T>(queryKey);
// Optimistically update
queryClient.setQueryData<T>(queryKey, (old) =>
old ? updateFn(old) : old
);
return { previousData };
},
onError: (error, variables, context) => {
// Rollback on error
if (context?.previousData) {
queryClient.setQueryData(queryKey, context.previousData);
}
},
onSettled: (data, error) => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey });
if (error) {
console.error('Mutation failed:', error);
}
},
});
}
}
6. File Upload Integration
File Upload with Progress
export const useFileUpload = () => {
const [progress, setProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const uploadFile = async (file: File, onProgress?: (progress: number) => void): Promise<string> => {
setIsUploading(true);
setProgress(0);
const formData = new FormData();
formData.append('file', file);
try {
const response = await apiClient.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
setProgress(progress);
onProgress?.(progress);
}
},
});
return response.data.fileId;
} finally {
setIsUploading(false);
}
};
return {
uploadFile,
progress,
isUploading,
};
};
// Component usage
export const DocumentUpload = () => {
const { uploadFile, progress, isUploading } = useFileUpload();
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
const fileId = await uploadFile(file, (progress) => {
console.log(`Upload progress: ${progress}%`);
});
console.log(`File uploaded with ID: ${fileId}`);
} catch (error) {
console.error('Upload failed:', error);
}
};
return (
<div>
<input
type="file"
onChange={handleFileSelect}
disabled={isUploading}
/>
{isUploading && (
<div>
<p>Uploading... {progress}%</p>
<div style={{ width: '100%', background: '#f0f0f0', borderRadius: '4px' }}>
<div
style={{
width: `${progress}%`,
height: '20px',
background: '#007bff',
borderRadius: '4px',
transition: 'width 0.3s ease'
}}
/>
</div>
</div>
)}
</div>
);
};
Performance Optimization
1. Request Batching
class RequestBatcher {
private batch: Array<{ url: string; config?: any; resolve: Function; reject: Function }> = [];
private batchTimeout: NodeJS.Timeout | null = null;
private readonly batchSize = 10;
private readonly batchDelay = 100; // ms
async request(url: string, config?: any): Promise<any> {
return new Promise((resolve, reject) => {
this.batch.push({ url, config, resolve, reject });
if (this.batch.length >= this.batchSize) {
this.flushBatch();
} else if (!this.batchTimeout) {
this.batchTimeout = setTimeout(() => this.flushBatch(), this.batchDelay);
}
});
}
private flushBatch(): void {
if (this.batch.length === 0) return;
const currentBatch = this.batch.splice(0, this.batchSize);
this.batchTimeout = null;
// Process batch
Promise.allSettled(
currentBatch.map(({ url, config, resolve, reject }) =>
apiClient.get(url, config)
.then(response => resolve(response))
.catch(error => reject(error))
)
).then((results) => {
results.forEach((result, index) => {
const { resolve, reject } = currentBatch[index];
if (result.status === 'fulfilled') {
resolve(result.value);
} else {
reject(result.reason);
}
});
});
}
}
export const batchedApiClient = new RequestBatcher();
2. Data Prefetching
export const usePrefetchQueries = () => {
const queryClient = useQueryClient();
const prefetchMarketData = useCallback((symbols: string[]) => {
symbols.forEach(symbol => {
queryClient.prefetchQuery({
queryKey: ['market-data', symbol],
queryFn: () => apiClient.get(`/market-data/${symbol}`).then(res => res.data),
staleTime: 5 * 60 * 1000, // 5 minutes
});
});
}, [queryClient]);
const prefetchUserOrders = useCallback((userId: string) => {
queryClient.prefetchQuery({
queryKey: ['orders', userId],
queryFn: () => apiClient.get('/orders', {
params: { userId, limit: 50 }
}).then(res => res.data),
staleTime: 2 * 60 * 1000, // 2 minutes
});
}, [queryClient]);
return { prefetchMarketData, prefetchUserOrders };
};
// Component that prefetches data on hover/focus
export const QuickOrderButton = ({ symbol }: { symbol: string }) => {
const { prefetchMarketData } = usePrefetchQueries();
const handleMouseEnter = useCallback(() => {
prefetchMarketData([symbol]);
}, [symbol, prefetchMarketData]);
return (
<button
onClick={() => openOrderForm(symbol)}
onMouseEnter={handleMouseEnter}
className="quick-order-btn"
>
Quick Order
</button>
);
};
Next: Deployment Architecture
Continue to Deployment Architecture for production deployment strategies and infrastructure management.