@cogs/fetch-client
v0.2.0
Published
Zero-dependency HTTP transport implementing the Kubb client contract — the single seam where base URL, auth, headers, timeouts and error shape live for generated API clients
Readme
@cogs/fetch-client
The HTTP transport seam that Kubb-generated API clients compile against. One place to configure base URL, auth, per-request headers, timeouts, credentials, and error shape — swap or instrument the transport without regenerating a single client.
Zero runtime dependencies (native fetch, AbortSignal.timeout, AbortSignal.any).
Node 20+ / modern browsers.
Why a seam
Generated clients should never import fetch or a specific HTTP library
directly. If they do, every regen re-bakes transport policy into hundreds of
files, and changing auth means changing generated code. Instead, Kubb is
pointed at this package:
// kubb.config.ts
pluginClient({ importPath: '@cogs/fetch-client', /* ... */ })
pluginReactQuery({ client: { importPath: '@cogs/fetch-client' } })Generated code then emits import fetch from '@cogs/fetch-client/client' and
types its requests with Client, RequestConfig, ResponseErrorConfig from
here.
Configure once at bootstrap
import { configureClient } from '@cogs/fetch-client'
configureClient({
baseUrl: process.env.NEXT_PUBLIC_API_URL!,
getToken: () => supabase.auth.getSession().then((s) => s.data.session?.access_token),
getHeaders: () => ({ 'X-Selected-Org-Id': selectedOrgId() }),
})Every generated call now flows through it. Call configureClient again to
rebuild after a token refresh or tenant switch.
Multi-API process, SSR request scope, or parallel tests? Use an isolated instance instead of the module singleton:
import { createFetchClient } from '@cogs/fetch-client'
const api = createFetchClient({ baseUrl })
await getFoo({ client: api.request })Error contract
Any non-2xx response and any transport failure throws FetchClientError:
.status— HTTP status, orundefinedfor network/timeout failures.error/.data— the parsed response body (structurally satisfiesResponseErrorConfig<T>, so generated call sites keep their types).url,.method,.headers
.status is exactly where @cogs/react-query's createQueryClient looks to
detect a 401 and trigger logout.
Body handling
- Plain objects → JSON, with
Content-Type: application/jsonset for you FormData/Blob/URLSearchParams/ typed arrays → passed through so the runtime sets the boundary/content-type- Query
paramsare serialised OpenAPI-style:null/undefineddropped, arrays repeat the key,Date→ ISO, nested objects → JSON
Optional: multi-service config registry + URL routing trie
Everything above assumes one API. If a process talks to several generated
(Kubb) APIs — each with its own base URL, auth, or headers — this package also
ships an optional, additive registry + routing layer. Single-API consumers can
ignore this section entirely and keep using configureClient.
The pieces:
- Config registry (
setConfig/getConfig/setConfigs/updateConfig/addConfig/getAllConfigs) — a genericRecord<string, Partial<ClientConfig>>keyed by whatever name you choose (a "confkey"). Ships empty; you decide the keys. - Operations registry (
addOperation/addOperations/getOperation/getAllOperations/updateOperation/deleteOperation) — a typed map of{ path, method, confkey, operationId? }, one entry per generated endpoint, pointing at which confkey it belongs to.addOperation/addOperationstake aConflictPolicy('throw'|'ignore'|'overwrite', default'throw') for what happens when a key is registered twice with a different definition. OperationTriePath— a URL + HTTP-method routing trie.initializeTrie(operations)builds a shared trie from an operations snapshot;findMatchingOperation(url, method)resolves the operation key for an outgoing request.
import {
setConfig,
addOperations,
getAllOperations,
initializeTrie,
findMatchingOperation,
getOperation,
getConfig,
} from '@cogs/fetch-client'
setConfig('envmgr', { baseUrl: 'https://envmgr.internal' })
setConfig('yellowpages', { baseUrl: 'https://yellowpages.internal' })
addOperations({
listEnvironments: { path: '/api/environments', method: 'get', confkey: 'envmgr' },
getService: { path: '/api/services/:id', method: 'get', confkey: 'yellowpages' },
})
initializeTrie(getAllOperations())
const opKey = findMatchingOperation('/api/services/42', 'GET') // 'getService'
const confkey = opKey ? getOperation(opKey)?.confkey : undefined // 'yellowpages'
const config = confkey ? getConfig(confkey) : undefined // { baseUrl: 'https://yellowpages.internal' }Pair config with createFetchClient (or a small wrapper of your own) to
build/select the right FetchClient instance per service. This layer is
pure bookkeeping — it does not itself perform requests.
