Skip to content

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:

typescript
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 guide

The 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.

typescript
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,
});
OptionTypePurpose
baseURLstringPrefixed in front of each request URL. A relative request URL is resolved against it.
bodyBodyInit | 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.
paramsRecord<string, any>Query-string parameters appended to the URL.
queryRecord<string, any>Alias for params; both are merged.
transformRequestTransformer | RequestTransformer[]Function(s) that reshape the outgoing body and headers. See Transformers.
responseTransformResponseTransformer | ResponseTransformer[]Function(s) (data, headers, status) that reshape the decoded response data (also for 4xx/5xx bodies; skipped for streams).
proxyProxyOptions | booleanProxy configuration. true (default) honours HTTP(S)_PROXY env vars; false disables. See Proxy & Environments.
timeoutnumberAbort 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:

typescript
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 ​

typescript
client.getBaseURL();                       // 'https://api.example.com/'
client.setBaseURL('https://api.acme.dev/'); // returns `this` — chainable

Managing 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:

typescript
client
    .setHeader('accept', 'application/json')
    .setHeader('x-tenant', 'acme');

client.getHeader('accept');   // 'application/json'
client.unsetHeader('x-tenant');
client.unsetHeaders();        // clear all default headers

For 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.

typescript
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 ​