feat(api-client): add @bytelyst/api-client package
- createApiClient() factory with baseUrl, getToken, defaultHeaders
- fetch() method that throws on error
- safeFetch() method that returns { data, error } tuple (never throws)
- Auto-injects Authorization header from getToken callback
- Replaces duplicated apiFetch patterns in 3 dashboards
This commit is contained in:
parent
602fa50216
commit
01624a2231
18
packages/api-client/package.json
Normal file
18
packages/api-client/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@bytelyst/api-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": ["dist"],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "vitest run"
|
||||||
|
}
|
||||||
|
}
|
||||||
93
packages/api-client/src/client.ts
Normal file
93
packages/api-client/src/client.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Configurable API client factory.
|
||||||
|
* Creates a fetch wrapper with base URL, auth token injection, and error handling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ApiClient, ApiClientConfig, ApiResult } from "./types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an API client with a base URL and optional auth token.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const api = createApiClient({
|
||||||
|
* baseUrl: "/api",
|
||||||
|
* getToken: () => localStorage.getItem("access_token"),
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Throws on error
|
||||||
|
* const users = await api.fetch<User[]>("/users");
|
||||||
|
*
|
||||||
|
* // Never throws
|
||||||
|
* const { data, error } = await api.safeFetch<User[]>("/users");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createApiClient(config: ApiClientConfig): ApiClient {
|
||||||
|
const { baseUrl, getToken, defaultHeaders } = config;
|
||||||
|
|
||||||
|
function buildHeaders(options?: RequestInit): HeadersInit {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...defaultHeaders,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (getToken) {
|
||||||
|
const token = getToken();
|
||||||
|
if (token) {
|
||||||
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.headers) {
|
||||||
|
const extra =
|
||||||
|
options.headers instanceof Headers
|
||||||
|
? Object.fromEntries((options.headers as any))
|
||||||
|
: Array.isArray(options.headers)
|
||||||
|
? Object.fromEntries(options.headers)
|
||||||
|
: (options.headers as Record<string, string>);
|
||||||
|
Object.assign(headers, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async fetch<T>(path: string, options?: RequestInit): Promise<T> {
|
||||||
|
const res = await globalThis.fetch(`${baseUrl}${path}`, {
|
||||||
|
...options,
|
||||||
|
headers: buildHeaders(options),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error(body.error || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
},
|
||||||
|
|
||||||
|
async safeFetch<T>(
|
||||||
|
path: string,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<ApiResult<T>> {
|
||||||
|
try {
|
||||||
|
const res = await globalThis.fetch(`${baseUrl}${path}`, {
|
||||||
|
...options,
|
||||||
|
headers: buildHeaders(options),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ error: res.statusText }));
|
||||||
|
return { data: null, error: body.error || `HTTP ${res.status}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as T;
|
||||||
|
return { data, error: null };
|
||||||
|
} catch {
|
||||||
|
return { data: null, error: "API unavailable" };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
2
packages/api-client/src/index.ts
Normal file
2
packages/api-client/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { createApiClient } from "./client.js";
|
||||||
|
export type { ApiClient, ApiClientConfig, ApiResult } from "./types.js";
|
||||||
22
packages/api-client/src/types.ts
Normal file
22
packages/api-client/src/types.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export interface ApiClientConfig {
|
||||||
|
baseUrl: string;
|
||||||
|
getToken?: () => string | null;
|
||||||
|
defaultHeaders?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResult<T> {
|
||||||
|
data: T | null;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiClient {
|
||||||
|
/**
|
||||||
|
* Fetch that throws on error — use when caller handles errors.
|
||||||
|
*/
|
||||||
|
fetch<T>(path: string, options?: RequestInit): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe fetch that never throws — returns { data, error } tuple.
|
||||||
|
*/
|
||||||
|
safeFetch<T>(path: string, options?: RequestInit): Promise<ApiResult<T>>;
|
||||||
|
}
|
||||||
10
packages/api-client/tsconfig.json
Normal file
10
packages/api-client/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"lib": ["ES2022", "DOM"]
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["src/**/*.test.ts"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user