176 lines
4.4 KiB
TypeScript
176 lines
4.4 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||
|
|
const response: AxiosResponse<T> = await this.client.get(url, config);
|
||
|
|
return response.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
async post<T = any, D = any>(
|
||
|
|
url: string,
|
||
|
|
data?: D,
|
||
|
|
config?: AxiosRequestConfig
|
||
|
|
): Promise<T> {
|
||
|
|
const response: AxiosResponse<T> = await this.client.post(url, data, config);
|
||
|
|
return response.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
async put<T = any, D = any>(
|
||
|
|
url: string,
|
||
|
|
data?: D,
|
||
|
|
config?: AxiosRequestConfig
|
||
|
|
): Promise<T> {
|
||
|
|
const response: AxiosResponse<T> = await this.client.put(url, data, config);
|
||
|
|
return response.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
async patch<T = any, D = any>(
|
||
|
|
url: string,
|
||
|
|
data?: D,
|
||
|
|
config?: AxiosRequestConfig
|
||
|
|
): Promise<T> {
|
||
|
|
const response: AxiosResponse<T> = await this.client.patch(url, data, config);
|
||
|
|
return response.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||
|
|
const response: AxiosResponse<T> = await this.client.delete(url, config);
|
||
|
|
return response.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
// File upload helper
|
||
|
|
async uploadFile<T = any>(
|
||
|
|
url: string,
|
||
|
|
file: File | FormData,
|
||
|
|
config?: AxiosRequestConfig
|
||
|
|
): Promise<T> {
|
||
|
|
const formData = file instanceof FormData ? file : new FormData();
|
||
|
|
if (file instanceof File) {
|
||
|
|
formData.append('file', file);
|
||
|
|
}
|
||
|
|
|
||
|
|
return this.post<T>(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;
|