@fetchkit/ffetch now exports named symbols only.
import { createClient } from '@fetchkit/ffetch'Feature plugins are exported from subpath entrypoints:
import {
dedupePlugin,
dedupeRequestHash,
} from '@fetchkit/ffetch/plugins/dedupe'
import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
import { requestShortcutsPlugin } from '@fetchkit/ffetch/plugins/request-shortcuts'
import { responseShortcutsPlugin } from '@fetchkit/ffetch/plugins/response-shortcuts'
import { downloadProgressPlugin } from '@fetchkit/ffetch/plugins/download-progress'Custom plugin authoring is documented in plugins.md.
Creates a new HTTP client instance.
import { createClient } from '@fetchkit/ffetch'
const client = createClient({
timeout: 5000,
retries: 2,
throwOnHttpError: true,
})| Option | Type | Default | Description |
|---|---|---|---|
timeout |
number (ms) |
5000 |
Whole-request timeout in milliseconds. Use 0 to disable timeout. |
retries |
number |
0 |
Maximum retry attempts. |
retryDelay |
number | (ctx: { attempt, request, response, error }) => number |
Exponential backoff + jitter | Delay between retries. |
shouldRetry |
(ctx: { attempt, request, response, error }) => boolean |
Retries on network errors, 5xx, 429 | Custom retry logic. |
throwOnHttpError |
boolean |
false |
If true, throws an HttpError for 4xx/5xx/429 after retries are exhausted. |
hooks |
{ before, after, onError, onRetry, onTimeout, onAbort, onComplete, transformRequest, transformResponse } |
{} |
Lifecycle hooks and transformers. |
fetchHandler |
(input: RequestInfo | URL, init?: RequestInit) => Promise<Response> |
global fetch |
Custom fetch-compatible implementation to wrap. |
plugins |
ClientPlugin[] |
[] |
Optional plugin list. Use this for dedupe, circuit breaker, and third-party features. |
import { createClient } from '@fetchkit/ffetch'
import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
const client = createClient({
plugins: [
dedupePlugin({
hashFn: (params) => `${params.method}|${params.url}|${params.body}`,
ttl: 30_000,
sweepInterval: 5_000,
}),
],
})import { createClient } from '@fetchkit/ffetch'
import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
const client = createClient({
plugins: [
circuitPlugin({
threshold: 5,
reset: 30_000,
onCircuitOpen: (req) => console.warn('Circuit opened:', req.url),
onCircuitClose: (req) => console.info('Circuit closed:', req.url),
}),
],
})
if (client.circuitOpen) {
console.warn('Circuit breaker is open')
}import { createClient } from '@fetchkit/ffetch'
import { requestShortcutsPlugin } from '@fetchkit/ffetch/plugins/request-shortcuts'
const client = createClient({
plugins: [requestShortcutsPlugin()],
})
const users = await client.get('https://api.example.com/users')
const created = await client.post('https://api.example.com/users', {
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
})Notes:
- The plugin is opt-in; default
createClient()behavior is unchanged. - Shortcut methods are available on the client instance:
get,post,put,patch,delete,head,options. - Each shortcut is equivalent to
client(url, { ...init, method: 'METHOD' }).
import { createClient } from '@fetchkit/ffetch'
import { responseShortcutsPlugin } from '@fetchkit/ffetch/plugins/response-shortcuts'
const client = createClient({
plugins: [responseShortcutsPlugin()],
})
const data = await client('https://api.example.com/users').json<
Array<{ id: number; name: string }>
>()
const html = await client('https://example.com/page').text()Notes:
- The plugin is opt-in; default
createClient()behavior is unchanged. await client(url)still returns a nativeResponse.- Shortcut methods are available on the returned request promise:
json,text,blob,arrayBuffer,formData.
import { createClient } from '@fetchkit/ffetch'
import { downloadProgressPlugin } from '@fetchkit/ffetch/plugins/download-progress'
const client = createClient({
plugins: [
downloadProgressPlugin((progress, chunk) => {
console.log(
`${(progress.percent * 100).toFixed(1)}% — ${progress.transferredBytes} bytes`
)
}),
],
})
const response = await client('https://example.com/large-file.zip')
await response.arrayBuffer() // drain the streamThe onProgress callback receives:
progress.percent— fraction from0to1. Always0whenContent-Lengthis absent.progress.transferredBytes— cumulative bytes received so far.progress.totalBytes— value ofContent-Lengthheader, or0if absent.chunk— the rawUint8Arraychunk just received.
Notes:
- The plugin is opt-in; default
createClient()behavior is unchanged. - The response body is fully stream-passthrough — callers can still read
.json(),.text(),.blob(), or.arrayBuffer()as normal. - If the response has no body (e.g.
204 No Content),onProgressis never called and the original response is returned unchanged.
Use the public plugin types from the root package and register your plugins via plugins.
import { createClient, type ClientPlugin } from '@fetchkit/ffetch'
const headerPlugin: ClientPlugin = {
name: 'header-plugin',
preRequest: (ctx) => {
ctx.request = new Request(ctx.request, {
headers: {
...Object.fromEntries(ctx.request.headers),
'x-trace-id': crypto.randomUUID(),
},
})
},
}
const client = createClient({
plugins: [headerPlugin],
})See plugins.md for full lifecycle, ordering, extensions, and advanced patterns.
The following top-level options were removed:
dedupededupeHashFndedupeTTLdedupeSweepIntervalcircuit
Use plugin modules instead via plugins: [...].
Core options can still be overridden per request:
await client('https://example.com/data', {
timeout: 1000,
retries: 0,
throwOnHttpError: true,
})type FFetch = {
(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
// Available when requestShortcutsPlugin() is installed
get?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
post?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
put?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
patch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
delete?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
head?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
options?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
pendingRequests: PendingRequest[]
abortAll: () => void
// Plugin extensions are composed into this type
}When the request shortcuts plugin is installed, the client instance is augmented with HTTP method shortcuts (for example client.get(url) and client.post(url, init)).
When the response shortcuts plugin is installed, the call return value is augmented with parsing shortcuts while preserving await client(url) as Response.
const client = createClient({
plugins: [responseShortcutsPlugin()] as const,
})
// Promise<Response> + shortcut methods
const data = await client('https://example.com/data').json<{ ok: boolean }>()
// Native behavior still works
const response = await client('https://example.com/data')| Option | Default Value / Logic |
|---|---|
timeout |
5000 ms |
retries |
0 |
retryDelay |
({ attempt }) => 2 ** attempt * 200 + Math.random() * 100 |
shouldRetry |
Retries on network errors, HTTP 5xx, or 429. Does not retry 4xx (except 429), abort, or timeout |
throwOnHttpError |
false |
hooks |
{} |
plugins |
[] |
- Signal combination (user, timeout, transformRequest) requires
AbortSignal.any. - The first retry decision uses
attempt = 1. - Plugin order is deterministic: first by
order, then by registration order.