api-client.ts60 lines · main
1export interface ApiCallOptions {
2 apiOrigin: string;
3 apiKey?: string;
4 bearer?: string;
5 method?: string;
6 body?: unknown;
7 headers?: Record<string, string>;
8}
9
10export class ApiCallError extends Error {
11 constructor(
12 readonly status: number,
13 readonly code: string,
14 message: string,
15 ) {
16 super(message);
17 this.name = 'ApiCallError';
18 }
19}
20
21export async function apiCall<T>(path: string, options: ApiCallOptions): Promise<T> {
22 const authHeaders: Record<string, string> = {};
23 if (options.bearer && options.bearer.length > 0) {
24 authHeaders.authorization = `Bearer ${options.bearer}`;
25 } else if (options.apiKey && options.apiKey.length > 0) {
26 authHeaders.authorization = `Bearer ${options.apiKey}`;
27 } else {
28 throw new Error('apiCall: either apiKey or bearer must be provided');
29 }
30
31 const headers: Record<string, string> = {
32 accept: 'application/json',
33 ...authHeaders,
34 ...(options.headers ?? {}),
35 };
36 if (options.body !== undefined) headers['content-type'] = 'application/json';
37
38 const res = await fetch(`${options.apiOrigin}${path}`, {
39 method: options.method ?? 'GET',
40 headers,
41 body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
42 });
43
44 if (!res.ok) {
45 const text = await res.text().catch(() => '');
46 let code = 'http_error';
47 let message = text || res.statusText;
48 try {
49 const parsed = JSON.parse(text) as { code?: string; message?: string };
50 if (parsed.code) code = parsed.code;
51 if (parsed.message) message = parsed.message;
52 } catch {
53 // leave defaults
54 }
55 throw new ApiCallError(res.status, code, message);
56 }
57
58 if (res.status === 204) return undefined as T;
59 return (await res.json()) as T;
60}