71 lines
2.0 KiB
TypeScript
71 lines
2.0 KiB
TypeScript
export interface ApiErrorShape {
|
|
error: string;
|
|
message: string;
|
|
details?: unknown;
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
readonly status: number;
|
|
readonly code: string;
|
|
readonly details?: unknown;
|
|
constructor(status: number, body: ApiErrorShape) {
|
|
super(body.message);
|
|
this.status = status;
|
|
this.code = body.error;
|
|
this.details = body.details;
|
|
}
|
|
}
|
|
|
|
interface RequestOptions<TBody = unknown> {
|
|
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
body?: TBody;
|
|
signal?: AbortSignal;
|
|
formData?: FormData;
|
|
}
|
|
|
|
async function request<TResponse, TBody = unknown>(
|
|
path: string,
|
|
options: RequestOptions<TBody> = {},
|
|
): Promise<TResponse> {
|
|
const headers: Record<string, string> = {};
|
|
let body: BodyInit | undefined;
|
|
|
|
if (options.formData) {
|
|
body = options.formData;
|
|
} else if (options.body !== undefined) {
|
|
headers['content-type'] = 'application/json';
|
|
body = JSON.stringify(options.body);
|
|
}
|
|
|
|
const response = await fetch(path, {
|
|
method: options.method ?? 'GET',
|
|
headers,
|
|
body,
|
|
credentials: 'include',
|
|
signal: options.signal,
|
|
});
|
|
|
|
const isJson = response.headers.get('content-type')?.includes('application/json');
|
|
const payload = isJson ? await response.json().catch(() => null) : null;
|
|
|
|
if (!response.ok) {
|
|
const errBody: ApiErrorShape =
|
|
payload && typeof payload === 'object' && 'error' in payload
|
|
? (payload as ApiErrorShape)
|
|
: { error: 'HTTP', message: response.statusText || 'Request failed' };
|
|
throw new ApiError(response.status, errBody);
|
|
}
|
|
|
|
return payload as TResponse;
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string, signal?: AbortSignal) => request<T>(path, { signal }),
|
|
post: <T, B = unknown>(path: string, body?: B) => request<T, B>(path, { method: 'POST', body }),
|
|
patch: <T, B = unknown>(path: string, body?: B) =>
|
|
request<T, B>(path, { method: 'PATCH', body }),
|
|
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
|
upload: <T>(path: string, formData: FormData) =>
|
|
request<T>(path, { method: 'POST', formData }),
|
|
};
|