Files
web-core/src/api/createApiClient.ts
Beatrice Dellacà cbabf43584
All checks were successful
continuous-integration/drone/push Build is passing
update prettier
2026-02-23 14:23:46 +01:00

135 lines
3.6 KiB
TypeScript

export type RequestOptions = {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
token?: string | null;
body?: unknown;
};
export type ResolveErrorInput = {
code?: string;
status?: number;
fallbackMessage?: string;
};
export type CreateApiClientConfig = {
baseUrl: string;
resolveError?: (input: ResolveErrorInput) => string;
inferErrorCodeFromStatus?: (status?: number | null) => string | undefined;
fetchImpl?: typeof fetch;
};
export class ApiError extends Error {
status: number;
code?: string;
requestId?: string;
details?: unknown;
rawMessage?: string;
constructor({
message,
status,
code,
requestId,
details,
rawMessage,
}: {
message: string;
status: number;
code?: string;
requestId?: string;
details?: unknown;
rawMessage?: string;
}) {
super(message);
this.name = 'ApiError';
this.status = status;
this.code = code;
this.requestId = requestId;
this.details = details;
this.rawMessage = rawMessage;
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value != null;
}
function parseErrorPayload(data: unknown) {
if (!isRecord(data)) {
return {
code: undefined as string | undefined,
rawMessage: undefined as string | undefined,
requestId: undefined as string | undefined,
details: undefined as unknown,
};
}
const code = typeof data.code === 'string' ? data.code : undefined;
const rawMessage = typeof data.error === 'string' ? data.error : undefined;
const requestId = typeof data.requestId === 'string' ? data.requestId : undefined;
const details = data.details;
return { code, rawMessage, requestId, details };
}
function defaultResolveError({ status, fallbackMessage }: ResolveErrorInput): string {
if (fallbackMessage) {
return fallbackMessage;
}
if (status != null) {
return `Request failed (${status}).`;
}
return 'Request failed. Please try again.';
}
export function createApiClient(config: CreateApiClientConfig) {
const {
baseUrl,
resolveError = defaultResolveError,
inferErrorCodeFromStatus,
fetchImpl,
} = config;
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { method = 'GET', token, body } = options;
const runFetch = fetchImpl ?? fetch;
const response = await runFetch(`${baseUrl}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await response.json().catch(() => null);
if (!response.ok) {
const parsed = parseErrorPayload(data);
const code = parsed.code ?? inferErrorCodeFromStatus?.(response.status);
const message = resolveError({
code,
status: response.status,
fallbackMessage: parsed.rawMessage,
});
throw new ApiError({
message,
status: response.status,
code,
requestId: parsed.requestId,
details: parsed.details,
rawMessage: parsed.rawMessage,
});
}
return data as T;
}
return {
request,
};
}