From 01624a22311b0591543c9fdc4bc0434c2a67500c Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 12 Feb 2026 11:20:04 -0800 Subject: [PATCH] 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 --- packages/api-client/package.json | 18 ++++++ packages/api-client/src/client.ts | 93 +++++++++++++++++++++++++++++++ packages/api-client/src/index.ts | 2 + packages/api-client/src/types.ts | 22 ++++++++ packages/api-client/tsconfig.json | 10 ++++ 5 files changed, 145 insertions(+) create mode 100644 packages/api-client/package.json create mode 100644 packages/api-client/src/client.ts create mode 100644 packages/api-client/src/index.ts create mode 100644 packages/api-client/src/types.ts create mode 100644 packages/api-client/tsconfig.json diff --git a/packages/api-client/package.json b/packages/api-client/package.json new file mode 100644 index 00000000..4b08b6fb --- /dev/null +++ b/packages/api-client/package.json @@ -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" + } +} diff --git a/packages/api-client/src/client.ts b/packages/api-client/src/client.ts new file mode 100644 index 00000000..cdae58b0 --- /dev/null +++ b/packages/api-client/src/client.ts @@ -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("/users"); + * + * // Never throws + * const { data, error } = await api.safeFetch("/users"); + * ``` + */ +export function createApiClient(config: ApiClientConfig): ApiClient { + const { baseUrl, getToken, defaultHeaders } = config; + + function buildHeaders(options?: RequestInit): HeadersInit { + const headers: Record = { + "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); + Object.assign(headers, extra); + } + + return headers; + } + + return { + async fetch(path: string, options?: RequestInit): Promise { + 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; + }, + + async safeFetch( + path: string, + options?: RequestInit, + ): Promise> { + 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" }; + } + }, + }; +} diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts new file mode 100644 index 00000000..47fd7f39 --- /dev/null +++ b/packages/api-client/src/index.ts @@ -0,0 +1,2 @@ +export { createApiClient } from "./client.js"; +export type { ApiClient, ApiClientConfig, ApiResult } from "./types.js"; diff --git a/packages/api-client/src/types.ts b/packages/api-client/src/types.ts new file mode 100644 index 00000000..338bbf3b --- /dev/null +++ b/packages/api-client/src/types.ts @@ -0,0 +1,22 @@ +export interface ApiClientConfig { + baseUrl: string; + getToken?: () => string | null; + defaultHeaders?: Record; +} + +export interface ApiResult { + data: T | null; + error: string | null; +} + +export interface ApiClient { + /** + * Fetch that throws on error — use when caller handles errors. + */ + fetch(path: string, options?: RequestInit): Promise; + + /** + * Safe fetch that never throws — returns { data, error } tuple. + */ + safeFetch(path: string, options?: RequestInit): Promise>; +} diff --git a/packages/api-client/tsconfig.json b/packages/api-client/tsconfig.json new file mode 100644 index 00000000..318c075a --- /dev/null +++ b/packages/api-client/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "lib": ["ES2022", "DOM"] + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +}