Add core Endpoint/ApiClient base classes

This commit is contained in:
2026-08-12 20:51:36 +02:00
parent ca08ed9294
commit 580357a130
3 changed files with 169 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
import { Endpoint } from './Endpoint';
export class ApiError extends Error {
constructor(
message: string,
readonly status: number,
readonly body: string
) {
super(message);
this.name = 'ApiError';
}
}
export abstract class ApiClient {
protected constructor(protected readonly baseUrl: string) {}
protected buildHeaders(): Record<string, string> {
return { Accept: 'application/json' };
}
protected async request<TResponse, TParams = void>(
endpoint: Endpoint<TResponse, TParams>,
params: TParams
): Promise<TResponse> {
const query = endpoint.query(params);
const qs = query ? `?${new URLSearchParams(query).toString()}` : '';
const url = `${this.baseUrl}${endpoint.path(params)}${qs}`;
const body = endpoint.body(params);
const headers = this.buildHeaders();
const init: RequestInit = { method: endpoint.method, headers };
if (body !== undefined) {
init.body = JSON.stringify(body);
headers['Content-Type'] = 'application/json';
}
const res = await fetch(url, init);
const text = await res.text();
const ok = res.ok || endpoint.acceptableStatuses.includes(res.status);
if (!ok) {
throw new ApiError(`${endpoint.method} ${url} returned ${res.status}`, res.status, text);
}
const json = text.length > 0 ? JSON.parse(text) : undefined;
return endpoint.parseResponse(json);
}
}

18
src/core/http/Endpoint.ts Normal file
View File

@@ -0,0 +1,18 @@
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
export abstract class Endpoint<TResponse, TParams = void> {
abstract readonly method: HttpMethod;
readonly acceptableStatuses: number[] = [];
abstract path(params: TParams): string;
query(_params: TParams): Record<string, string> | undefined {
return undefined;
}
body(_params: TParams): unknown {
return undefined;
}
abstract parseResponse(json: unknown): TResponse;
}