@gavoryn/clearfetch
v1.0.6
Published
A dependency-free, fetch-native HTTP client for modern JavaScript and TypeScript runtimes.
Readme
clearfetch
A dependency-free, fetch-native HTTP client for modern JavaScript and TypeScript runtimes.
npm install @gavoryn/clearfetchWhy clearfetch?
Use clearfetch when you want a thin layer over native fetch, not a separate transport abstraction.
Choose it when you want:
- reusable client defaults for
baseURL, headers, timeout, retries, and hooks - JSON request/response convenience without runtime dependencies
- predictable typed errors instead of repeating the same
fetchboilerplate - a small surface area that is easy to audit
Not a fit if...
clearfetch is intentionally narrow. It is probably not the right client if you need:
- upload or download progress APIs
- interceptor-style response rewriting or a middleware ecosystem
- legacy CommonJS or old-runtime support
- automatic caching, cookie jars, XSRF helpers, or transport adapters
- a broader, older, more feature-rich abstraction like axios
Hooks are intentionally not axios-style interceptors.
Usage
One-off request
import { request } from '@gavoryn/clearfetch'
const user = await request<{ id: string; name: string }>(
'https://api.example.com/users/123',
)Reusable client
import { createClient } from '@gavoryn/clearfetch'
const api = createClient({
baseURL: 'https://api.example.com',
headers: {
Accept: 'application/json',
},
timeout: 5_000,
})
const user = await api.get<{ id: string; name: string }>('/users/123')Query parameters
import { createClient } from '@gavoryn/clearfetch'
const api = createClient({
baseURL: 'https://api.example.com',
})
const users = await api.get('/users', {
query: {
active: true,
tag: ['admin', 'editor'],
},
})
const ordered = await api.get('/users', {
query: new URLSearchParams('tag=admin&page=1&tag=editor'),
})Use an object for ordinary query parameters. Use native URLSearchParams when duplicate-key ordering matters.
JSON request bodies
import { createClient } from '@gavoryn/clearfetch'
const api = createClient({
baseURL: 'https://api.example.com',
})
const created = await api.post<{ id: string }>('/users', {
json: {
name: 'Ada Lovelace',
role: 'admin',
},
})If json is provided, clearfetch:
- serializes the value with
JSON.stringify() - sets
Content-Type: application/jsonif it is not already present - rejects the request with
ConfigErrorifbodyis also provided
Use body directly only when you want to send a raw payload such as FormData, URLSearchParams, or pre-serialized text.
Raw body payloads
import { createClient } from '@gavoryn/clearfetch'
const api = createClient({
baseURL: 'https://api.example.com',
})
const form = new FormData()
form.set('avatar', fileInput.files[0])
await api.post('/profile/avatar', {
body: form,
})Extended client defaults
import { createClient } from '@gavoryn/clearfetch'
const api = createClient({
baseURL: 'https://api.example.com',
})
const authed = api.extend({
headers: {
Authorization: 'Bearer token',
},
})
const profile = await authed.get('/me')Conservative retries
import { createClient } from '@gavoryn/clearfetch'
const api = createClient({
baseURL: 'https://api.example.com',
retry: {
attempts: 3,
backoffMs: 200,
maxBackoffMs: 1_000,
retryOnMethods: ['GET', 'HEAD'],
retryOnStatuses: [429, 503],
},
})
const response = await api.get('/status')Retries are disabled by default. When enabled, they are intentionally conservative and do not allow streaming request bodies. They are a convenience for bounded retry cases, not a general resilience framework.
Abort a request
import {
AbortRequestError,
createClient,
} from '@gavoryn/clearfetch'
const controller = new AbortController()
const api = createClient({
baseURL: 'https://api.example.com',
})
const promise = api.get('/reports/current', {
signal: controller.signal,
})
controller.abort(new Error('user cancelled'))
try {
await promise
} catch (error) {
if (error instanceof AbortRequestError) {
console.error('Request cancelled', error.cause)
}
}Hooks
const api = createClient({
hooks: {
beforeRequest: [
async (context) => {
context.headers.set('x-client', 'clearfetch')
},
],
afterResponse: [
async (context) => {
console.log(context.response.status)
},
],
onError: [
async (context) => {
console.error(context.error)
},
],
},
})beforeRequest hook failures, request-normalization failures, retry rebuild
failures, and request-construction failures propagate as-is and are observable
through onError before being re-thrown. afterResponse hooks receive a
cloned Response, so reading the body there does not consume the response used
for normal parsing or HttpError creation.
Hook scope is intentionally narrow:
beforeRequestmay mutate headers and may replace the URL with a final absolute URLafterResponseandonErrorare observational only apart from throwingcontext.optionsis read-only hook metadata, not a supported mutation surface
Cloned afterResponse inspection is intended for ordinary API payloads, not large streaming or heavy binary workflows.
Safe diagnostic header logging
clearfetch has no built-in logging or telemetry. Applications that log request
diagnostics can use redactHeaders() to copy headers and replace common
sensitive values before writing application-owned diagnostics.
By default, it redacts exact case-insensitive matches for authorization,
cookie, set-cookie, proxy-authorization, x-api-key, and api-key.
import { createClient, redactHeaders } from '@gavoryn/clearfetch'
const api = createClient({
hooks: {
beforeRequest: [
(context) => {
const safeHeaders = redactHeaders(context.headers)
console.log(Object.fromEntries(safeHeaders))
},
],
},
})Error handling
import {
HttpError,
ParseError,
TimeoutError,
createClient,
isHttpClientError,
} from '@gavoryn/clearfetch'
const api = createClient({
baseURL: 'https://api.example.com',
})
try {
await api.get('/users/123')
} catch (error) {
if (!isHttpClientError(error)) {
throw error
} else if (error instanceof HttpError) {
console.error(error.status, error.bodyText)
} else if (error instanceof ParseError) {
console.error(error.bodyText)
} else if (error instanceof TimeoutError) {
console.error(error.timeout)
}
}Text and raw responses
import { createClient } from '@gavoryn/clearfetch'
const api = createClient({
baseURL: 'https://api.example.com',
})
const health = await api.get('/health', {
responseType: 'text',
})
const rawResponse = await api.get('/download', {
responseType: 'raw',
})Runtime validation
TypeScript generics describe the expected response shape, but they do not validate response data at runtime.
import { z } from 'zod'
import { createClient } from '@gavoryn/clearfetch'
const User = z.object({
id: z.string(),
name: z.string(),
})
const api = createClient({
baseURL: 'https://api.example.com',
})
const data: unknown = await api.get<unknown>('/users/123')
const user = User.parse(data)If you need end-to-end runtime safety, validate parsed data with a schema library such as Zod or Valibot after the request resolves.
Behavior notes
- Non-2xx responses throw
HttpError. HttpError.bodyTextcapture is bounded and may be truncated for very large payloads.ParseError.bodyTextcapture is also bounded and may be truncated for very large invalid JSON payloads.HttpError.responseremains available for status, headers, and metadata, but its body may already be consumed or canceled by diagnosticbodyTextcapture.- JSON mode returns
undefinedfor empty response bodies. - In JSON mode, successful empty bodies resolve as
T | undefined. - No default timeout is applied. Requests run until completion or external abort unless
timeoutis configured. - Invalid request configuration, including invalid hook lists, fails fast with
ConfigError. - Hook, request-normalization, retry rebuild, and request-construction failures are not wrapped as
NetworkError. afterResponsereceives a clonedResponsefor safe inspection.- Relative request inputs require
baseURL. beforeRequestmay override the URL only with a final absolute URL.beforeRequestmay mutate headers, but hook option metadata is read-only.- Retry support is opt-in and conservative by default.
- Retry support does not allow streaming request bodies.
- The
jsonhelper serializes request bodies and setsContent-Type: application/jsonwhen absent. bodyandjsoncannot be used together.- TypeScript rejects common invalid option combinations such as
bodyplusjson, and request bodies onGET/HEADrequest shapes. Runtime validation still protects JavaScript callers. - The package performs no telemetry or hidden network activity beyond the caller's request.
Advanced behavior notes
- Timeout windows are per attempt when retries are enabled. A configured
timeoutis not a total deadline across all retry attempts. - External abort signals surface as
AbortRequestError, including when the signal was aborted with a custom reason. - External abort beats timeout if it happens first; timeout beats external abort if the timeout fires first.
- Timeout aborts surface as
TimeoutError. - External abort reasons are preserved as
AbortRequestError.causewhen the platform exposes them. - Retry backoff waits are abortable.
- Timeout windows start after
beforeRequesthooks complete. - Retry backoff waits do not consume per-attempt timeout windows.
- If
beforeRequestreplacescontext.url, that replacement is final. Previously resolvedbaseURLand query parameters are not reapplied to the replacement URL. - Hook metadata includes
context.options.attemptandcontext.options.maxAttempts. Non-retried requests report attempt1and max attempts1. - When
queryserializes to a non-empty string, hook metadata includescontext.options.queryStringwithout a leading?. Existing search parameters from the input URL remain visible oncontext.url.
Important limitations by design
- The package stays close to native
fetchrather than inventing a separate transport model. - Hooks are intentionally narrower than axios-style interceptors.
- Retries are conservative and explicit, not aggressive or automatic.
- The package is ESM-only and targets modern runtimes only.
- The public API is intentionally small; missing features are often deliberate non-goals, not incomplete work.
Supported runtimes
clearfetch currently supports:
- Node.js
18.xand newer - modern browsers with native
fetch,Request,Response,Headers,URL, andAbortController
The package is ESM-only and does not target legacy runtimes or polyfill-driven environments.
Security
- The package includes no built-in telemetry.
- The package performs no hidden network activity beyond the caller's request.
- Vulnerability reports should follow the policy in SECURITY.md.
Release and CI
- CI lints GitHub Actions workflows before merge.
- CI runs lint, test, and build checks on selected supported Node.js versions.
- CI also runs a lightweight browser-like test path using
happy-domon Node.js20. - Dependency review is enforced for pull requests and supports manual base/head validation.
- The release workflow supports a non-publishing dry-run path via manual dispatch.
- npm publishing now uses npm trusted publishing from GitHub Actions instead of a long-lived publish token.
- The release workflow publishes to npm with provenance and creates or verifies the matching GitHub Release record.
- Normal releases are expected to publish from GitHub Actions, not from local machines.
- Release and repository protection policy is documented in RELEASE.md.
Package surface
The public package surface is intentionally narrow:
- the root export provides the supported runtime API and public types
- internal implementation modules are not part of the supported import contract
- the package includes no lifecycle scripts and is intended to publish only built
dist/artifacts
Development
npm install: install development dependenciesnpm run build: compile the package intodist/npm run check:package-metadata: validate publish metadata and zero-runtime-dependency posturenpm run check:pack-smoke: smoke-test the packed tarball from a clean temporary installnpm run lint: run TypeScript static checksnpm test: run the test suitenpm run test:browser-like: run browser-like package entrypoint coverage withhappy-dom
Status
clearfetch is published as @gavoryn/clearfetch. The main branch may be
ahead of the latest npm package until a matching release tag runs the Release
workflow. Check npm and GitHub Releases for the currently published version.
Project goals and behavior are documented in PURPOSE.md and DESIGN.md.
