// Error shape returned by API handlers on a 4xx/5xx response (see `ErrorDto` on the backend). export interface ApiErrorBody { error: string } export class ApiError extends Error { constructor(message: string) { super(message) this.name = 'ApiError' } } // Builds a `URLSearchParams` from an object of optional query fields, skipping any that are // `undefined` (falsy strings like `''` are also skipped, matching the hand-written `if` checks // this replaces). export function buildSearchParams(fields: Record): URLSearchParams { const search = new URLSearchParams() for (const [key, value] of Object.entries(fields)) { if (value !== undefined && value !== '') search.set(key, String(value)) } return search } export async function getJson(path: string): Promise { const res = await fetch(path) if (!res.ok) { let message = `${path} failed: ${res.status}` try { const body: unknown = await res.json() if (body && typeof body === 'object' && 'error' in body && typeof (body as { error: unknown }).error === 'string') { message = (body as { error: string }).error } } catch { // body wasn't JSON (or had no `error` field) — fall back to the generic message } throw new Error(message) } return res.json() as Promise } // Throws `ApiError` (not `Error`) with the parsed `{ "error": "..." }` message so callers can surface it inline. export async function postJson(path: string, body: unknown): Promise { const res = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) { let message = `${path} failed: ${res.status}` try { const errorBody = (await res.json()) as Partial if (errorBody && typeof errorBody.error === 'string') { message = errorBody.error } } catch { // response body wasn't JSON; fall back to the generic message } throw new ApiError(message) } return res.json() as Promise } // Same error handling as `postJson` (throws `ApiError` with the parsed message) but for a PUT — used where updating in place, rather than creating, is the operation. export async function putJson(path: string, body: unknown): Promise { const res = await fetch(path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) { let message = `${path} failed: ${res.status}` try { const errorBody = (await res.json()) as Partial if (errorBody && typeof errorBody.error === 'string') { message = errorBody.error } } catch { // response body wasn't JSON; fall back to the generic message } throw new ApiError(message) } return res.json() as Promise } export async function sendJson(path: string, method: string, body?: unknown): Promise { const res = await fetch(path, { method, headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined, body: body !== undefined ? JSON.stringify(body) : undefined, }) if (!res.ok) { let message = `${path} failed: ${res.status}` try { const data = await res.json() if (data?.error) message = data.error } catch { // response body wasn't JSON (or was empty) — fall back to the generic message } throw new Error(message) } if (res.status === 204) return undefined as T return res.json() as Promise }