Start integrating the onboarding flow with backend 6
This commit is contained in:
176
frontend/src/api/client/apiClient.ts
Normal file
176
frontend/src/api/client/apiClient.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 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;
|
||||
2
frontend/src/api/client/index.ts
Normal file
2
frontend/src/api/client/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { apiClient, default } from './apiClient';
|
||||
export type { ApiError } from './apiClient';
|
||||
Reference in New Issue
Block a user