Files
tvone/lib/api-client.ts
T

110 lines
3.0 KiB
TypeScript
Raw Normal View History

2026-04-23 10:09:44 +01:00
export type ApiMethod = "GET" | "POST" | "PATCH" | "DELETE";
export interface ApiClientConfig {
baseUrl: string;
getAccessToken?: () => string | Promise<string | undefined> | undefined;
}
export class ApiError extends Error {
status: number;
body: unknown;
constructor(status: number, message: string, body: unknown) {
super(message);
this.status = status;
this.body = body;
}
}
export class ApiClient {
private baseUrl: string;
private getAccessToken?: ApiClientConfig["getAccessToken"];
constructor(config: ApiClientConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
this.getAccessToken = config.getAccessToken;
}
private async buildHeaders(extra?: HeadersInit): Promise<HeadersInit> {
const token = this.getAccessToken ? await this.getAccessToken() : undefined;
return {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...extra,
};
}
private buildUrl(path: string, query?: Record<string, string | number | boolean | undefined>) {
const url = new URL(`${this.baseUrl}${path}`);
if (query) {
for (const [k, v] of Object.entries(query)) {
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
}
}
return url.toString();
}
async request<TResponse, TBody = undefined>(params: {
method: ApiMethod;
path: string;
query?: Record<string, string | number | boolean | undefined>;
body?: TBody;
headers?: HeadersInit;
isFormData?: boolean;
}): Promise<TResponse> {
const { method, path, query, body, headers, isFormData } = params;
const finalHeaders = await this.buildHeaders(headers);
const response = await fetch(this.buildUrl(path, query), {
method,
headers: isFormData ? finalHeaders : { "Content-Type": "application/json", ...finalHeaders },
body: body
? isFormData
? (body as unknown as FormData)
: JSON.stringify(body)
: undefined,
});
const text = await response.text();
const data = text ? safeJsonParse(text) : null;
if (!response.ok) {
throw new ApiError(response.status, response.statusText, data);
}
return data as TResponse;
}
get<TResponse>(path: string, query?: Record<string, string | number | boolean | undefined>) {
return this.request<TResponse>({ method: "GET", path, query });
}
post<TResponse, TBody = undefined>(
path: string,
body?: TBody,
options?: { headers?: HeadersInit; isFormData?: boolean }
) {
return this.request<TResponse, TBody>({
method: "POST",
path,
body,
headers: options?.headers,
isFormData: options?.isFormData,
});
}
patch<TResponse, TBody = undefined>(path: string, body?: TBody) {
return this.request<TResponse, TBody>({ method: "PATCH", path, body });
}
delete<TResponse>(path: string) {
return this.request<TResponse>({ method: "DELETE", path });
}
}
function safeJsonParse(text: string) {
try {
return JSON.parse(text);
} catch {
return text;
}
}