extract auth to lib
This commit is contained in:
134
src/api/createApiClient.ts
Normal file
134
src/api/createApiClient.ts
Normal 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user