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:
saravanakumardb1 2026-02-12 11:20:04 -08:00
parent 602fa50216
commit 01624a2231
5 changed files with 145 additions and 0 deletions

View 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"
}
}

View 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" };
}
},
};
}

View File

@ -0,0 +1,2 @@
export { createApiClient } from "./client.js";
export type { ApiClient, ApiClientConfig, ApiResult } from "./types.js";

View 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>>;
}

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"lib": ["ES2022", "DOM"]
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}