// On the server (SSR / route handlers / RSC) prefer INTERNAL_API_URL — Docker compose service name. // In the browser, NEXT_PUBLIC_API_URL is baked in at build time and points at the host. const rawBaseUrl = typeof window === "undefined" ? process.env.INTERNAL_API_URL ?? process.env.NEXT_PUBLIC_API_URL : process.env.NEXT_PUBLIC_API_URL; const baseUrl = (rawBaseUrl ?? "http://localhost:8000").replace(/\/$/, ""); export class ApiError extends Error { constructor(public status: number, message: string, public body?: unknown) { super(message); this.name = "ApiError"; } } type FetchInit = RequestInit & { token?: string; locale?: string }; async function request(path: string, init: FetchInit = {}): Promise { const { token, locale, headers, ...rest } = init; const res = await fetch(`${baseUrl}${path.startsWith("/") ? path : `/${path}`}`, { ...rest, headers: { "Content-Type": "application/json", ...(locale ? { "Accept-Language": locale } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}), ...headers, }, }); if (!res.ok) { let body: unknown; try { body = await res.json(); } catch { body = await res.text().catch(() => undefined); } throw new ApiError(res.status, `${res.status} ${res.statusText}`, body); } if (res.status === 204) return undefined as T; return res.json() as Promise; } export const api = { baseUrl, get: (path: string, init?: FetchInit) => request(path, { ...init, method: "GET" }), post: (path: string, body?: unknown, init?: FetchInit) => request(path, { ...init, method: "POST", body: body ? JSON.stringify(body) : undefined }), patch: (path: string, body?: unknown, init?: FetchInit) => request(path, { ...init, method: "PATCH", body: body ? JSON.stringify(body) : undefined }), delete: (path: string, init?: FetchInit) => request(path, { ...init, method: "DELETE" }), };