The Client
The Client class is the entire base library. You construct one with a set of defaults, and every request merges its per-call options over those defaults.
Constructing a client
There are three equivalent ways to get a client:
import { Client, createClient, useClient } from 'hapic';
// 1. directly
const a = new Client({ baseURL: 'https://api.example.com/' });
// 2. factory (identical, reads nicer)
const b = createClient({ baseURL: 'https://api.example.com/' });
// 3. a named singleton from the registry
const c = useClient(); // see the Instance Registry guideThe constructor takes a RequestBaseOptions object. Every field is optional.
Configuration options
RequestBaseOptions extends the platform RequestInit, so all standard fetch options (headers, credentials, mode, cache, signal, …) are available too, plus the hapic-specific additions below.
import { createClient, ResponseType } from 'hapic';
const client = createClient({
// --- standard RequestInit (passed through to fetch) ---
headers: { accept: 'application/json' },
credentials: 'include',
// --- hapic additions ---
baseURL: 'https://api.example.com/',
responseType: ResponseType.JSON,
params: { tenant: 'acme' },
proxy: true,
timeout: 10_000,
transform: (body, headers) => body,
responseTransform: (data) => data,
});| Option | Type | Purpose |
|---|---|---|
baseURL | string | Prefixed in front of each request URL. A relative request URL is resolved against it. |
body | BodyInit | Record<string, any> | Default request body. Plain objects are serialized by the default transformer. |
responseType | 'json' | 'text' | 'blob' | 'arrayBuffer' | 'stream' | Force how the response body is decoded. Defaults to detection from the Content-Type header. |
params | Record<string, any> | Query-string parameters appended to the URL. |
query | Record<string, any> | Alias for params; both are merged. |
transform | RequestTransformer | RequestTransformer[] | Function(s) that reshape the outgoing body and headers. See Transformers. |
responseTransform | ResponseTransformer | ResponseTransformer[] | Function(s) (data, headers, status) that reshape the decoded response data (also for 4xx/5xx bodies; skipped for streams). |
proxy | ProxyOptions | boolean | Proxy configuration. true (default) honours HTTP(S)_PROXY env vars; false disables. See Proxy & Environments. |
timeout | number | Abort the request after this many milliseconds. Only a finite number above 0 arms it; absent, 0, a negative number or NaN mean no timeout. A fraction is rounded up and a value above 2147483647 (about 24.8 days) is capped to it. Covers reading the body too (a stream keeps the timer running while consumed). A timed-out request rejects with code 'ETIMEDOUT', as does a caller signal aborted with a TimeoutError; any other abort through the caller's signal stays 'ECONNABORTED' (see Timeouts). A per-request undefined keeps the client default. Never passed to fetch. |
RequestInit lives here too
Because the options object is a RequestInit, anything fetch accepts works: method, credentials, mode, cache, redirect, signal (for AbortController), and so on.
Defaults vs. per-request options
The options you pass to the constructor become client.defaults. Each request shallow-merges its own options on top:
const client = createClient({
baseURL: 'https://api.example.com/',
headers: { accept: 'application/json' },
});
// uses the default baseURL and accept header,
// adds a per-request query and overrides responseType
const { data } = await client.get('report.csv', {
query: { from: '2026-01-01' },
responseType: 'text',
});Per-request headers are merged over the client's default headers (the request wins on conflicts). Other options are replaced wholesale.
Reading & changing the base URL
client.getBaseURL(); // 'https://api.example.com/'
client.setBaseURL('https://api.acme.dev/'); // returns `this` — chainableManaging headers
setHeader / getHeader / unsetHeader operate on the client's default headers; they apply to every upcoming request. All header setters return this, so they chain:
client
.setHeader('accept', 'application/json')
.setHeader('x-tenant', 'acme');
client.getHeader('accept'); // 'application/json'
client.unsetHeader('x-tenant');
client.unsetHeaders(); // clear all default headersFor authorization specifically, prefer the dedicated helpers covered in Headers & Authorization.
Copying the configuration
toOptions() returns the client's current configuration as constructor options: the defaults, the current headers (including ones set or unset after construction) and the transport.
const copy = new Client(client.toOptions());The options and headers are copies, so changing one client does not affect the other: arrays and plain objects among the options (transform, responseTransform, params, query, a proxy options object) are copied one level deep. Functions, a signal and other class instances are passed on as they are, and the transport is the same instance. Hooks are not included; register them on the new client.
What's next
- Request Methods — the verbs you'll call most.
- Instance Registry — reuse one configured client across modules.