// Thin fetch wrapper used by every React page. Centralises auth // header handling (cookie-based, credentials: 'include'), JSON // parsing, and typed success-vs-error narrowing via shared/types. import type { ApiResponse } from '@/shared/types'; export class ApiError extends Error { constructor(public status: number, message: string) { super(message); } } export async function apiFetch( path: string, init: RequestInit = {}, ): Promise { const resp = await fetch(path, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...(init.headers || {}), }, ...init, }); // Non-JSON responses (e.g. audio blobs) — caller must handle. const ct = resp.headers.get('content-type') || ''; if (!ct.includes('application/json')) { if (!resp.ok) throw new ApiError(resp.status, resp.statusText); return (await resp.blob()) as unknown as TOk; } const body = (await resp.json()) as ApiResponse; if (!resp.ok || body.success === false) { throw new ApiError(resp.status, (body as { error?: string }).error || resp.statusText); } return body as unknown as TOk; } // Shortcuts for common verbs export const api = { get: (path: string) => apiFetch(path), post: (path: string, body: unknown) => apiFetch(path, { method: 'POST', body: JSON.stringify(body) }), put: (path: string, body: unknown) => apiFetch(path, { method: 'PUT', body: JSON.stringify(body) }), delete: (path: string) => apiFetch(path, { method: 'DELETE' }), };