60 lines
1.6 KiB
TypeScript
60 lines
1.6 KiB
TypeScript
const DEFAULT_API_BASE_URL = 'http://127.0.0.1:8100/api';
|
|
|
|
const trimTrailingSlash = (value: string): string => value.replace(/\/+$/, '');
|
|
|
|
const getApiBaseUrl = (): string => {
|
|
const configured = import.meta.env.VITE_API_BASE_URL || DEFAULT_API_BASE_URL;
|
|
return trimTrailingSlash(configured);
|
|
};
|
|
export { getApiBaseUrl };
|
|
|
|
type RequestOptions = {
|
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
body?: unknown;
|
|
signal?: AbortSignal;
|
|
headers?: Record<string, string>;
|
|
};
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
export const apiRequest = async <T>(path: string, options: RequestOptions = {}): Promise<T> => {
|
|
const url = `${getApiBaseUrl()}${path.startsWith('/') ? path : `/${path}`}`;
|
|
|
|
const response = await fetch(url, {
|
|
method: options.method || 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...options.headers,
|
|
},
|
|
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
signal: options.signal,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let message = `Request failed: ${response.status}`;
|
|
try {
|
|
const errorData = await response.json();
|
|
if (errorData?.detail) {
|
|
message = typeof errorData.detail === 'string' ? errorData.detail : message;
|
|
}
|
|
} catch {
|
|
// Ignore parsing errors.
|
|
}
|
|
throw new ApiError(message, response.status);
|
|
}
|
|
|
|
if (response.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return response.json() as Promise<T>;
|
|
};
|