/** * Core HTTP client for React Query integration * * Architecture: * - Axios: HTTP client for making requests * - This Client: Handles auth tokens, tenant context, and error formatting * - Services: Business logic that uses this client * - React Query Hooks: Data fetching layer that uses services * * React Query doesn't replace HTTP clients - it manages data fetching/caching/sync */ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'; export interface ApiError { message: string; status?: number; code?: string; details?: any; } class ApiClient { private client: AxiosInstance; private baseURL: string; private authToken: string | null = null; private tenantId: string | null = null; constructor(baseURL: string = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api/v1') { this.baseURL = baseURL; this.client = axios.create({ baseURL: this.baseURL, timeout: 30000, headers: { 'Content-Type': 'application/json', }, }); this.setupInterceptors(); } private setupInterceptors() { // Request interceptor to add auth headers this.client.interceptors.request.use( (config) => { if (this.authToken) { config.headers.Authorization = `Bearer ${this.authToken}`; } if (this.tenantId) { config.headers['X-Tenant-ID'] = this.tenantId; } return config; }, (error) => { return Promise.reject(this.handleError(error)); } ); // Response interceptor for error handling this.client.interceptors.response.use( (response) => response, (error) => { return Promise.reject(this.handleError(error)); } ); } private handleError(error: AxiosError): ApiError { if (error.response) { // Server responded with error status const { status, data } = error.response; return { message: (data as any)?.detail || (data as any)?.message || `Request failed with status ${status}`, status, code: (data as any)?.code, details: data, }; } else if (error.request) { // Network error return { message: 'Network error - please check your connection', status: 0, }; } else { // Other error return { message: error.message || 'Unknown error occurred', }; } } // Configuration methods setAuthToken(token: string | null) { this.authToken = token; } setTenantId(tenantId: string | null) { this.tenantId = tenantId; } getAuthToken(): string | null { return this.authToken; } getTenantId(): string | null { return this.tenantId; } // HTTP Methods - Return direct data for React Query async get(url: string, config?: AxiosRequestConfig): Promise { const response: AxiosResponse = await this.client.get(url, config); return response.data; } async post( url: string, data?: D, config?: AxiosRequestConfig ): Promise { const response: AxiosResponse = await this.client.post(url, data, config); return response.data; } async put( url: string, data?: D, config?: AxiosRequestConfig ): Promise { const response: AxiosResponse = await this.client.put(url, data, config); return response.data; } async patch( url: string, data?: D, config?: AxiosRequestConfig ): Promise { const response: AxiosResponse = await this.client.patch(url, data, config); return response.data; } async delete(url: string, config?: AxiosRequestConfig): Promise { const response: AxiosResponse = await this.client.delete(url, config); return response.data; } // File upload helper async uploadFile( url: string, file: File | FormData, config?: AxiosRequestConfig ): Promise { const formData = file instanceof FormData ? file : new FormData(); if (file instanceof File) { formData.append('file', file); } return this.post(url, formData, { ...config, headers: { ...config?.headers, 'Content-Type': 'multipart/form-data', }, }); } // Raw axios instance for advanced usage getAxiosInstance(): AxiosInstance { return this.client; } } // Create and export singleton instance export const apiClient = new ApiClient(); export default apiClient;