56 lines
2.2 KiB
TypeScript
56 lines
2.2 KiB
TypeScript
/**
|
|
* The HTTP client. One place that knows the base URL.
|
|
*
|
|
* Deliberately dependency-free: the scaffold installs no axios, so an `api.ts`
|
|
* that imported it would fail to build. The shape (`.get(...).then(r => r.data)`)
|
|
* matches what the fullstack skill's examples use.
|
|
*
|
|
* VITE_API_URL is the backend BASE, wired at deploy time. Endpoints live under
|
|
* `/api`. Never hardcode localhost — it works in dev and breaks on deploy.
|
|
*/
|
|
const BASE = (import.meta.env.VITE_API_URL ?? '').replace(/\/$/, '');
|
|
|
|
export type Res<T> = { data: T; status: number };
|
|
|
|
// Set by an auth store (the `auth` block) — every request then carries it as a
|
|
// Bearer header. Nothing else in the app needs to know how auth travels.
|
|
let authToken: string | null = null;
|
|
export const setAuthToken = (token: string | null): void => {
|
|
authToken = token;
|
|
};
|
|
|
|
async function request<T>(method: string, path: string, body?: unknown): Promise<Res<T>> {
|
|
const headers: Record<string, string> = {};
|
|
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
if (!res.ok) {
|
|
let detail = `${res.status} ${res.statusText}`;
|
|
try {
|
|
const j = (await res.json()) as { detail?: unknown };
|
|
if (typeof j.detail === 'string') detail = j.detail;
|
|
} catch {
|
|
/* keep the status line */
|
|
}
|
|
throw new Error(detail);
|
|
}
|
|
// 204 has no body — reading it as JSON throws, and a delete that "fails"
|
|
// because it succeeded is a confusing bug to chase.
|
|
const data = res.status === 204 ? (undefined as T) : ((await res.json()) as T);
|
|
return { data, status: res.status };
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string) => request<T>('GET', path),
|
|
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body ?? {}),
|
|
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body ?? {}),
|
|
patch: <T>(path: string, body?: unknown) => request<T>('PATCH', path, body ?? {}),
|
|
delete: <T>(path: string) => request<T>('DELETE', path),
|
|
};
|
|
|
|
export const apiBaseUrl = () => BASE;
|