/** * Shared API Client Factory * Creates a configured API client for making authenticated requests. */ import type { ApiResponse, FetchOptions } from './types'; export interface ApiClientConfig { /** Base URL for the API (e.g., 'http://localhost:3002') */ baseUrl: string; /** Optional API prefix (default: '/api') */ apiPrefix?: string; /** Function to get the current auth token */ getToken?: () => Promise | string | null; /** Whether running in browser environment */ isBrowser?: boolean; /** Local storage key for token fallback */ tokenStorageKey?: string; } export interface ApiClient { /** Make a GET request */ get: (endpoint: string, options?: Omit) => Promise>; /** Make a POST request */ post: ( endpoint: string, body?: unknown, options?: Omit ) => Promise>; /** Make a PUT request */ put: ( endpoint: string, body?: unknown, options?: Omit ) => Promise>; /** Make a PATCH request */ patch: ( endpoint: string, body?: unknown, options?: Omit ) => Promise>; /** Make a DELETE request */ delete: (endpoint: string, options?: Omit) => Promise>; /** Make a request with any method */ request: (endpoint: string, options?: FetchOptions) => Promise>; /** Upload a single file */ uploadFile: (endpoint: string, file: File, token?: string) => Promise>; /** Upload multiple files */ uploadFiles: (endpoint: string, files: File[], token?: string) => Promise>; } /** * Create an API client with the given configuration. */ export function createApiClient(config: ApiClientConfig): ApiClient { const { baseUrl, apiPrefix = '/api', getToken, isBrowser = true, tokenStorageKey } = config; async function getAuthToken(providedToken?: string): Promise { if (providedToken) return providedToken; if (getToken) { const token = await getToken(); if (token) return token; } // Fallback to localStorage if in browser and key provided if (isBrowser && tokenStorageKey && typeof localStorage !== 'undefined') { return localStorage.getItem(tokenStorageKey) || undefined; } return undefined; } async function request(endpoint: string, options: FetchOptions = {}): Promise> { const { method = 'GET', body, token, isFormData = false, headers: customHeaders } = options; const authToken = await getAuthToken(token); try { const headers: Record = { ...customHeaders }; // Don't set Content-Type for FormData - browser sets it automatically with boundary if (!isFormData) { headers['Content-Type'] = 'application/json'; } if (authToken) { headers['Authorization'] = `Bearer ${authToken}`; } const url = `${baseUrl}${apiPrefix}${endpoint}`; const response = await fetch(url, { method, headers, body: isFormData ? (body as FormData) : body ? JSON.stringify(body) : undefined, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); return { data: null, error: new Error(errorData.message || `API error: ${response.status}`), }; } // Handle empty responses (204 No Content) if (response.status === 204) { return { data: null, error: null }; } const data = await response.json(); return { data, error: null }; } catch (error) { return { data: null, error: error instanceof Error ? error : new Error('Unknown error'), }; } } async function uploadFile( endpoint: string, file: File, token?: string ): Promise> { const authToken = await getAuthToken(token); try { const formData = new FormData(); formData.append('file', file); const headers: Record = {}; if (authToken) { headers['Authorization'] = `Bearer ${authToken}`; } const response = await fetch(`${baseUrl}${apiPrefix}${endpoint}`, { method: 'POST', headers, body: formData, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); return { data: null, error: new Error(errorData.message || `Upload error: ${response.status}`), }; } const data = await response.json(); return { data, error: null }; } catch (error) { return { data: null, error: error instanceof Error ? error : new Error('Upload failed'), }; } } async function uploadFiles( endpoint: string, files: File[], token?: string ): Promise> { const authToken = await getAuthToken(token); try { const formData = new FormData(); files.forEach((file) => { formData.append('files', file); }); const headers: Record = {}; if (authToken) { headers['Authorization'] = `Bearer ${authToken}`; } const response = await fetch(`${baseUrl}${apiPrefix}${endpoint}`, { method: 'POST', headers, body: formData, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); return { data: null, error: new Error(errorData.message || `Upload error: ${response.status}`), }; } const data = await response.json(); return { data, error: null }; } catch (error) { return { data: null, error: error instanceof Error ? error : new Error('Upload failed'), }; } } return { get: (endpoint: string, options?: Omit) => request(endpoint, { ...options, method: 'GET' }), post: (endpoint: string, body?: unknown, options?: Omit) => request(endpoint, { ...options, method: 'POST', body }), put: (endpoint: string, body?: unknown, options?: Omit) => request(endpoint, { ...options, method: 'PUT', body }), patch: (endpoint: string, body?: unknown, options?: Omit) => request(endpoint, { ...options, method: 'PATCH', body }), delete: (endpoint: string, options?: Omit) => request(endpoint, { ...options, method: 'DELETE' }), request, uploadFile, uploadFiles, }; }