import { BACKEND_URL } from '../utils/config'; import type { ApiError } from '../types/api'; import { store } from '../store'; type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; interface RequestOptions { method?: HttpMethod; body?: unknown; headers?: Record; requireAuth?: boolean; } /** * API Client for making requests to the backend * Handles authentication, error handling, and response typing */ class ApiClient { private baseUrl: string; constructor(baseUrl: string) { this.baseUrl = baseUrl; } /** * Get the current JWT token from Redux store */ private getToken(): string | null { return store.getState().auth.token; } /** * Build headers for the request */ private buildHeaders(options: RequestOptions): HeadersInit { const headers: Record = { 'Content-Type': 'application/json', ...options.headers, }; // Add authorization header if auth is required if (options.requireAuth !== false) { const token = this.getToken(); if (token) { headers.Authorization = `Bearer ${token}`; } } return headers; } /** * Make an API request */ private async request( endpoint: string, options: RequestOptions = {} ): Promise { const { method = 'GET', body, requireAuth = true } = options; const url = `${this.baseUrl}${endpoint}`; const headers = this.buildHeaders({ ...options, requireAuth }); const config: RequestInit = { method, headers, }; if (body && method !== 'GET') { config.body = JSON.stringify(body); } try { const response = await fetch(url, config); // Handle non-JSON responses const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return {} as T; } const data = await response.json(); // Handle error responses if (!response.ok) { const error: ApiError = data.error ? { success: false, error: data.error, message: data.message } : { success: false, error: `HTTP ${response.status}: ${response.statusText}` }; throw error; } return data as T; } catch (error) { // Re-throw API errors as-is if (error && typeof error === 'object' && 'error' in error) { throw error; } // Wrap network/other errors throw { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred', } as ApiError; } } /** * GET request */ async get(endpoint: string, options?: Omit): Promise { return this.request(endpoint, { ...options, method: 'GET' }); } /** * POST request */ async post( endpoint: string, body?: unknown, options?: Omit ): Promise { return this.request(endpoint, { ...options, method: 'POST', body }); } /** * PUT request */ async put( endpoint: string, body?: unknown, options?: Omit ): Promise { return this.request(endpoint, { ...options, method: 'PUT', body }); } /** * PATCH request */ async patch( endpoint: string, body?: unknown, options?: Omit ): Promise { return this.request(endpoint, { ...options, method: 'PATCH', body }); } /** * DELETE request */ async delete(endpoint: string, options?: Omit): Promise { return this.request(endpoint, { ...options, method: 'DELETE' }); } } // Export singleton instance export const apiClient = new ApiClient(BACKEND_URL);