- Added @eslint/js dependency - Updated eslint.config.js for ESLint 9 compatibility - Added required globals (crypto, localStorage, React, etc.) - Fixed unused imports and variables - Disabled sort-imports temporarily - Formatted all files with Prettier
89 lines
2.4 KiB
TypeScript
89 lines
2.4 KiB
TypeScript
/**
|
|
* Configurable API client factory.
|
|
* Creates a fetch wrapper with base URL, auth token injection, and error handling.
|
|
*/
|
|
|
|
import type { ApiClient, ApiClientConfig, ApiResult } from './types.js';
|
|
|
|
/**
|
|
* Create an API client with a base URL and optional auth token.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const api = createApiClient({
|
|
* baseUrl: "/api",
|
|
* getToken: () => localStorage.getItem("access_token"),
|
|
* });
|
|
*
|
|
* // Throws on error
|
|
* const users = await api.fetch<User[]>("/users");
|
|
*
|
|
* // Never throws
|
|
* const { data, error } = await api.safeFetch<User[]>("/users");
|
|
* ```
|
|
*/
|
|
export function createApiClient(config: ApiClientConfig): ApiClient {
|
|
const { baseUrl, getToken, defaultHeaders } = config;
|
|
|
|
function buildHeaders(options?: RequestInit): HeadersInit {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...defaultHeaders,
|
|
};
|
|
|
|
if (getToken) {
|
|
const token = getToken();
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
}
|
|
|
|
if (options?.headers) {
|
|
const extra =
|
|
options.headers instanceof Headers
|
|
? Object.fromEntries(options.headers as any)
|
|
: Array.isArray(options.headers)
|
|
? Object.fromEntries(options.headers)
|
|
: (options.headers as Record<string, string>);
|
|
Object.assign(headers, extra);
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
return {
|
|
async fetch<T>(path: string, options?: RequestInit): Promise<T> {
|
|
const res = await globalThis.fetch(`${baseUrl}${path}`, {
|
|
...options,
|
|
headers: buildHeaders(options),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
throw new Error(body.error || `HTTP ${res.status}`);
|
|
}
|
|
|
|
return res.json() as Promise<T>;
|
|
},
|
|
|
|
async safeFetch<T>(path: string, options?: RequestInit): Promise<ApiResult<T>> {
|
|
try {
|
|
const res = await globalThis.fetch(`${baseUrl}${path}`, {
|
|
...options,
|
|
headers: buildHeaders(options),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
return { data: null, error: body.error || `HTTP ${res.status}` };
|
|
}
|
|
|
|
const data = (await res.json()) as T;
|
|
return { data, error: null };
|
|
} catch {
|
|
return { data: null, error: 'API unavailable' };
|
|
}
|
|
},
|
|
};
|
|
}
|