@lcabrera/api
v0.2.0
Published
Browser-safe HTTP building blocks: API base-URL resolution, validated fetch, and paginated distinct-value queries.
Maintainers
Readme
@lcabrera/api
Browser-safe HTTP building blocks: resolving an API base URL across the environments a React SSR app actually runs in, fetching JSON and validating it before it reaches your types, and paging a distinct-values endpoint for filter dropdowns.
No Node, no database driver, no framework. It runs in a browser, in a service
worker, and during SSR — and the TypeScript config enforces that rather than
trusting it: types omits node, so a stray process or fs reach-in fails
typecheck in CI.
Install
npm install @lcabrera/apiNo peer dependencies. @lcabrera/utils comes along as a regular dependency.
Why it exists
This package used to be half of @lcabrera/server. The two halves — browser
fetch helpers and Node/Postgres access — sat together because nothing depended
on only one of them, until @lcabrera/ui came to need exactly two helpers from
it. That meant every consumer of a UI component library was installing the
Postgres driver.
Splitting on runtime removes that at the package boundary instead of papering
over it, and the split is enforced from both sides: this package's tsconfig
denies Node globals, and @lcabrera/ui's publish gate fails if anything in its
dependency closure contains a node:* import.
Exports
Every helper is a separate subpath — no barrel — so you pull in exactly what you use and tests mock a module rather than a barrel.
| Import | What it does |
| --------------------------------------------------------- | ------------------------------------------------------------------------------- |
| @lcabrera/api/http/fetch-and-validate.util | Fetch → assert OK → parse JSON → validate through a type guard, with a timeout |
| @lcabrera/api/http/build-paginated-query-params.util | Builds the shared limit/skip/sort/filter query string |
| @lcabrera/api/config/get-api-base-url.util | Resolves the API base URL across SSR, dev-proxy, private-IP and production |
| @lcabrera/api/config/config.constants | API_SERVER_PORT and the CONFIG per-environment host map |
| @lcabrera/api/config/config.types | ApiConfig — the shape of CONFIG |
| @lcabrera/api/distinct/fetch-distinct-values.util | Pages a distinct-values endpoint (the HTTP half of a filter-options descriptor) |
| @lcabrera/api/distinct/is-distinct-values-response.util | Type guard for DistinctValuesResponse |
| @lcabrera/api/distinct/parse-filter-options-params.util | Parses filter-option search params into fetchDistinctValues arguments |
| @lcabrera/api/distinct/distinct.types | DistinctValuesResponse — the wire contract |
Usage
Fetch something and know what you got
fetchAndValidate refuses to hand back a value it has not checked. The guard is
yours, so the validation library is your choice — or no library at all.
import { fetchAndValidate } from '@lcabrera/api/http/fetch-and-validate.util';
type Order = { readonly id: number; readonly total: number };
const isOrderList = (value: unknown): value is readonly Order[] =>
Array.isArray(value) && value.every((item) => typeof item === 'object');
const orders = await fetchAndValidate({
isValid: isOrderList,
shapeErrorMessage: 'Unexpected /orders response shape',
timeoutMs: 10_000,
url: '/api/orders',
});It throws on a non-OK status, on unparseable JSON, and on a body the guard rejects — so downstream code never has to re-check.
Resolve the base URL you should be calling
The awkward part of an SSR app is that "where is the API" has a different answer
in the browser, on the server, behind a dev proxy, and on a LAN IP.
getApiBaseUrl encodes all of them; pass the request URL when you have one
(loaders, actions) and it derives the answer from the host actually being served.
import { getApiBaseUrl } from '@lcabrera/api/config/get-api-base-url.util';
export const loader = async ({ request }: { request: Request }) => {
const baseUrl = getApiBaseUrl(request.url);
// → dev proxy, localhost, same-host:3001, or the production host
};Page a filter dropdown
import { fetchDistinctValues } from '@lcabrera/api/distinct/fetch-distinct-values.util';
const { hasMore, values } = await fetchDistinctValues({
baseUrl: getApiBaseUrl(),
columnName: 'country',
limit: 50,
offset: 0,
tableName: 'orders',
});Guarantees
- Browser-safe, enforced by the compiler — no
node:*, noprocess, nofs; the tsconfig omits Node types so a reach-in fails typecheck. - Explicit per-file subpaths, no barrel — import the module you need.
- Relative imports carry explicit
.tsextensions, so the package resolves under both bundler mode andmoduleResolution: NodeNext. - Published as compiled ESM (
.mjs+.d.mts) with source maps and"sideEffects": false, mirroring the source tree one file per module. - 95% coverage gate — the build fails below it.
Links
- Repository —
packages/api - Changelog
- Companion packages:
@lcabrera/utils(pure helpers),@lcabrera/server(the Node half),@lcabrera/ui(React components)
MIT © Lucio Cabrera
