export class ApiError extends Error { statusCode: number; details?: unknown; constructor( message: string, statusCode: number, details?: unknown ) { super(message); this.name = 'ApiError'; this.statusCode = statusCode; this.details = details; } } interface ErrorResponseBody { error?: string; details?: unknown; } import { getApiBaseUrl } from './config'; export class ApiClient { private baseUrl: string; constructor(baseUrl: string = getApiBaseUrl()) { this.baseUrl = baseUrl; } getBaseURL(): string { return this.baseUrl; } private async request( endpoint: string, options?: RequestInit ): Promise { const url = `${this.baseUrl}${endpoint}`; try { const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers, }, }); if (!response.ok) { const errorData: ErrorResponseBody = await response.json().catch(() => ({})); throw new ApiError( errorData.error || `HTTP ${response.status}: ${response.statusText}`, response.status, errorData ); } // Handle 204 No Content — intentional no-data for void operations if (response.status === 204) { return undefined as unknown as T; } return response.json(); } catch (error) { if (error instanceof ApiError) { throw error; } throw new ApiError( error instanceof Error ? error.message : 'Network error', 0 ); } } async get(endpoint: string): Promise { return this.request(endpoint, { method: 'GET' }); } async post(endpoint: string, data?: unknown): Promise { return this.request(endpoint, { method: 'POST', body: data ? JSON.stringify(data) : undefined, }); } async put(endpoint: string, data?: unknown): Promise { return this.request(endpoint, { method: 'PUT', body: data ? JSON.stringify(data) : undefined, }); } async patch(endpoint: string, data?: unknown): Promise { return this.request(endpoint, { method: 'PATCH', body: data ? JSON.stringify(data) : undefined, }); } async delete(endpoint: string): Promise { return this.request(endpoint, { method: 'DELETE' }); } async getText(endpoint: string): Promise { const url = `${this.baseUrl}${endpoint}`; const response = await fetch(url); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new ApiError( errorData.error || `HTTP ${response.status}: ${response.statusText}`, response.status, errorData ); } return response.text(); } async putText(endpoint: string, text: string): Promise<{ message?: string; [key: string]: unknown }> { const url = `${this.baseUrl}${endpoint}`; const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'text/plain' }, body: text, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new ApiError( errorData.error || `HTTP ${response.status}: ${response.statusText}`, response.status, errorData ); } return response.json(); } } export const apiClient = new ApiClient();