@alphinex/api
v1.1.0
Published
Transport-agnostic HTTP client contracts, interceptor pipeline, TanStack Query integration.
Readme
@alphinex/api
Transport-agnostic HTTP client contracts: a fetch-based ApiClient with an ordered
request/response interceptor pipeline, a normalized ApiError, a pre-configured TanStack Query
QueryClient factory, and a query-key convention. This package deliberately knows nothing about
any specific backend — auth headers, CSRF, and error-shape mapping are supplied by a concrete
adapter such as @alphinex/api-laravel via the interceptor options below.
createApiClient(options)
Builds an ApiClient with get/post/put/patch/delete, all returning Promise<T>. Request
bodies are JSON-serialized and Content-Type/Accept: application/json are set by default.
Non-2xx responses are normalized into an ApiError; a 204 resolves to undefined.
import { createApiClient } from "@alphinex/api";
const client = createApiClient({
baseUrl: "https://api.example.com",
requestInterceptors: [
(config) => ({ ...config, headers: { ...config.headers, "X-App-Version": "1.0.0" } }),
],
responseInterceptors: [
(response) => {
if (response.status === 401) {
// e.g. redirect to login
}
return response;
},
],
});
const user = await client.get<{ id: string; name: string }>("/user");
await client.post("/invoices", { amount: 100 });requestInterceptors run in order over a RequestConfig (url/method/headers/body/signal)
before the request is sent; responseInterceptors run in order over the raw Response before
error normalization, so they can inspect status codes, throw their own ApiError to short-circuit
the generic normalization, or trigger a token refresh and return a replacement Response. Pass
fetchFn to override the global fetch (useful for mocking in tests, or a non-global-fetch
runtime).
ApiError
The normalized error every request rejects with (extends @alphinex/core's AppError). Carries
status (HTTP status code), code (a machine-readable string, e.g. http_404), and an optional
fieldErrors (Record<string, string[]>) that a backend adapter populates for validation
failures:
import { ApiError } from "@alphinex/api";
try {
await client.post("/invoices", payload);
} catch (error) {
if (error instanceof ApiError && error.status === 422) {
console.log(error.fieldErrors); // { amount: ["The amount field is required."] }
} else if (error instanceof ApiError && error.status === 401) {
// session expired
} else {
throw error;
}
}createQueryClient(options?)
A TanStack Query QueryClient with platform defaults: retries skip 4xx errors except
401/408/429 (transient or refresh-recoverable), and refetchOnWindowFocus is off to avoid
surprising refetches in dashboard-heavy apps. Tune defaultStaleTimeMs (default 30_000) and
maxRetries (default 3):
import { createQueryClient } from "@alphinex/api";
import { QueryClientProvider } from "@tanstack/react-query";
const queryClient = createQueryClient({ defaultStaleTimeMs: 60_000, maxRetries: 2 });
function Root() {
return (
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
}queryKeys
A query-key factory convention that prevents key collisions/typos across a large app.
queryKeys.resource(name) returns all(), lists(), list(params?), details(), and
detail(id), each producing a stable, ordered key array:
import { queryKeys } from "@alphinex/api";
import { useQuery } from "@tanstack/react-query";
useQuery({
queryKey: queryKeys.resource("invoices").detail(invoiceId),
queryFn: () => client.get(`/invoices/${invoiceId}`),
});
useQuery({
queryKey: queryKeys.resource("invoices").list({ status: "overdue" }),
queryFn: () => client.get("/invoices", { headers: { "X-Filter": "overdue" } }),
});See documentation/ARCHITECTURE.md for the full package contract, dependency rules, and roadmap placement.
uploadFile — uploads with progress
fetch still cannot report upload progress in any shipping browser. uploadFile is
XMLHttpRequest-based for that single reason, and normalises everything else — errors, aborts,
JSON parsing — into the same shapes the rest of this package produces.
import { uploadFile } from "@alphinex/api";
const saved = await uploadFile<{ id: string }>({
url: "https://api.example.com/files",
file,
onProgress: ({ percent }) => setProgress(percent),
signal: controller.signal,
});Failures reject with ApiError, including a fieldErrors map for a Laravel-style 422 — so an
upload failure is handled exactly like any other request failure:
try {
await uploadFile({ url, file });
} catch (error) {
if (error instanceof ApiError && error.status === 422) {
setErrors(error.fieldErrors);
}
}percent is 0 whenever the total length isn't computable, rather than a guess.
createUploader binds a base URL and shared headers, the way createApiClient binds fetch:
const upload = createUploader({
baseUrl: "https://api.example.com",
headers: { Authorization: `Bearer ${token}` },
withCredentials: true,
});
await upload("files", { file, onProgress });Content-Type is never set from headers — the browser must append the multipart boundary
itself, and overriding it produces a body the server cannot parse.
