// API — typed fetch wrapper. Auto-injects the session token, throws on // non-2xx with a parsed error, and supports AbortSignal for cancellation. // // Replace `apiBaseURL` with your backend root. The Resources route shows the // typical usage pattern. import { loadSession, signOut } from "~/lib/session" export const apiBaseURL = "/api" export class ApiError extends Error { status: number body: unknown constructor(message: string, status: number, body: unknown) { super(message) this.name = "ApiError" this.status = status this.body = body } } export type ApiInit = Omit & { method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" body?: unknown } export async function apiFetch( path: string, init: ApiInit = {}, ): Promise { const session = loadSession() const headers = new Headers(init.headers) if (session?.token) headers.set("Authorization", `Bearer ${session.token}`) if (init.body !== undefined && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json") } const url = path.startsWith("http") ? path : `${apiBaseURL}${path}` const res = await fetch(url, { ...init, method: init.method ?? "GET", headers, body: init.body === undefined ? undefined : typeof init.body === "string" ? init.body : JSON.stringify(init.body), }) if (res.status === 401) { // Token rejected — clear session so the shell bounces to /login. signOut() } const ct = res.headers.get("Content-Type") ?? "" const parsed = ct.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null) if (!res.ok) { const message = (parsed && typeof parsed === "object" && "message" in parsed ? String((parsed as { message: unknown }).message) : null) ?? `${res.status} ${res.statusText}` throw new ApiError(message, res.status, parsed) } return parsed as T } /** Convenience helpers. */ export const api = { get: (path: string, init?: ApiInit) => apiFetch(path, { ...init, method: "GET" }), post: (path: string, body?: unknown, init?: ApiInit) => apiFetch(path, { ...init, method: "POST", body }), put: (path: string, body?: unknown, init?: ApiInit) => apiFetch(path, { ...init, method: "PUT", body }), patch: (path: string, body?: unknown, init?: ApiInit) => apiFetch(path, { ...init, method: "PATCH", body }), del: (path: string, init?: ApiInit) => apiFetch(path, { ...init, method: "DELETE" }), }