base-api.ts•2.75 kB
import fetch, { Response } from "node-fetch";
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
export interface RequestOptions {
method?: HttpMethod;
headers?: Record<string, string>;
query?: Record<string, string | number | boolean | undefined>;
body?: unknown;
timeoutMs?: number;
}
export class BaseApiClient {
protected readonly baseUrl: string;
protected readonly defaultHeaders: Record<string, string>;
constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {
this.baseUrl = baseUrl.replace(/\/$/, "");
this.defaultHeaders = defaultHeaders;
}
protected buildUrl(path: string, query?: RequestOptions["query"]): string {
const url = new URL(path.replace(/^\//, ""), this.baseUrl + "/");
if (query) {
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
});
}
return url.toString();
}
protected async request<T = unknown>(path: string, options: RequestOptions = {}): Promise<T> {
const { method = "GET", headers = {}, query, body, timeoutMs } = options;
const url = this.buildUrl(path, query);
const controller = new AbortController();
const timeout = timeoutMs ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
try {
// Determine request body and headers
let requestBody: any = undefined;
let finalHeaders: Record<string, string> = {
"content-type": "application/json",
...this.defaultHeaders,
...headers,
};
if (body !== undefined) {
if (typeof body === "string" || body instanceof URLSearchParams || (typeof Buffer !== "undefined" && Buffer.isBuffer(body))) {
requestBody = body as any;
// do not force JSON content-type for non-JSON bodies
} else {
requestBody = JSON.stringify(body);
finalHeaders["content-type"] = finalHeaders["content-type"] || "application/json";
}
}
const response: Response = await fetch(url, {
method,
headers: finalHeaders,
body: requestBody,
signal: controller.signal,
} as any);
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status} ${response.statusText}: ${text}`);
}
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
return (await response.json()) as T;
}
// Fallback to text
return (await response.text()) as unknown as T;
} finally {
if (timeout) clearTimeout(timeout);
}
}
}