Files
bakery-ia/frontend/src/api/client/apiClient.ts

176 lines
4.4 KiB
TypeScript
Raw Normal View History

/**
* 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;