useAPI.js
// Generic fetch wrapper with error handling
async function fetchJSON(url, options) {
const res = await fetch(url, {
...options,
credentials: 'same-origin', // Ensure cookies are sent
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `HTTP ${res.status}`);
}
if (res.status === 204) {
return undefined;
}
// Check content-type to avoid parsing HTML as JSON
const contentType = res.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error(`Expected JSON but got ${contentType || 'unknown'}`);
}
return res.json();
}
// API mutation functions
export async function api(method, url, body) {
return fetchJSON(url, {
method,
body: body ? JSON.stringify(body) : undefined,
});
}
export const apiGet = (url) => api('GET', url);
export const apiPost = (url, body) => api('POST', url, body);
export const apiPut = (url, body) => api('PUT', url, body);
export const apiPatch = (url, body) => api('PATCH', url, body);
export const apiDelete = (url) => api('DELETE', url);
// File upload
export async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/files', {
method: 'POST',
body: formData,
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `HTTP ${res.status}`);
}
return res.json();
}