extract auth to lib

This commit is contained in:
2026-02-22 20:37:30 +01:00
parent db6813cab1
commit 9f86fe80d7
24 changed files with 2442 additions and 0 deletions

134
src/api/createApiClient.ts Normal file
View File

@@ -0,0 +1,134 @@
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
};
}