/** * The answer of a validate endpoint: problems maps a field of the validated value to a message; message describes what * the value would break elsewhere, or is empty. */ export type Validation = { problems: Record message: string } /** A failed call: the server's message and status, with the problems and message of a refused write (422). */ export class ApiError extends Error { status: number problems: Record detail: string constructor(message: string, status: number, problems: Record = {}, detail = '') { super(message) this.status = status this.problems = problems this.detail = detail } } /** Calls the backend and returns the decoded JSON; a failed call throws ApiError with the backend's message. */ export async function request(method: string, path: string, body?: unknown): Promise { const response = await fetch(path, { method, headers: body === undefined ? {} : { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body), }) if (!response.ok) { const payload = (await response.json().catch(() => null)) as { error?: string; problems?: Record; message?: string } | null throw new ApiError(payload?.error ?? `${method} ${path} failed (${response.status}).`, response.status, payload?.problems, payload?.message) } return (await response.json()) as T }