/** * Configurable API client factory. * Creates a fetch wrapper with base URL, auth token injection, and error handling. */ import type { ApiClient, ApiClientConfig, ApiResult } from './types.js'; /** * Create an API client with a base URL and optional auth token. * * @example * ```ts * const api = createApiClient({ * baseUrl: "/api", * getToken: () => localStorage.getItem("access_token"), * }); * * // Throws on error * const users = await api.fetch("/users"); * * // Never throws * const { data, error } = await api.safeFetch("/users"); * ``` */ export function createApiClient(config: ApiClientConfig): ApiClient { const { baseUrl, getToken, defaultHeaders } = config; function buildHeaders(options?: RequestInit): HeadersInit { const headers: Record = { 'Content-Type': 'application/json', ...defaultHeaders, }; if (getToken) { const token = getToken(); if (token) { headers['Authorization'] = `Bearer ${token}`; } } if (options?.headers) { const extra = options.headers instanceof Headers ? Object.fromEntries(options.headers as any) : Array.isArray(options.headers) ? Object.fromEntries(options.headers) : (options.headers as Record); Object.assign(headers, extra); } return headers; } return { async fetch(path: string, options?: RequestInit): Promise { const res = await globalThis.fetch(`${baseUrl}${path}`, { ...options, headers: buildHeaders(options), }); if (!res.ok) { const body = await res.json().catch(() => ({ error: res.statusText })); throw new Error(body.error || `HTTP ${res.status}`); } return res.json() as Promise; }, async safeFetch(path: string, options?: RequestInit): Promise> { try { const res = await globalThis.fetch(`${baseUrl}${path}`, { ...options, headers: buildHeaders(options), }); if (!res.ok) { const body = await res.json().catch(() => ({ error: res.statusText })); return { data: null, error: body.error || `HTTP ${res.status}` }; } const data = (await res.json()) as T; return { data, error: null }; } catch { return { data: null, error: 'API unavailable' }; } }, }; }