From version 5 ffetch uses a plugin pipeline for optional behavior such as deduplication and circuit breaking.
- Keep the core client small.
- Make optional features tree-shakeable.
- Support first-party and third-party extensions.
Plugins can hook into request execution at multiple phases:
setup(once, at client creation): register client extensions.preRequest(per request, before dispatch): validate or prepare request context.wrapDispatch(per request): wrap the network dispatch function.onSuccess(per request): run after successful completion.onError(per request): run after failure.onFinally(per request): always run when request settles.
Execution order is deterministic:
- Plugins are sorted by
order(ascending). - For equal
order, registration order is preserved.
import { createClient } from '@fetchkit/ffetch'
import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
const client = createClient({
plugins: [
dedupePlugin({ ttl: 30_000, sweepInterval: 5_000 }),
circuitPlugin({ threshold: 5, reset: 30_000 }),
],
})Use the public ClientPlugin type.
import { createClient, type ClientPlugin } from '@fetchkit/ffetch'
type TimingExtension = {
lastDurationMs: number
}
function timingPlugin(): ClientPlugin<TimingExtension> {
let lastDurationMs = 0
return {
name: 'timing',
order: 100,
setup: ({ defineExtension }) => {
defineExtension('lastDurationMs', {
get: () => lastDurationMs,
})
},
preRequest: (ctx) => {
ctx.state.start = Date.now()
},
onFinally: (ctx) => {
const start =
typeof ctx.state.start === 'number' ? ctx.state.start : Date.now()
lastDurationMs = Date.now() - start
},
}
}
const client = createClient({
plugins: [timingPlugin()] as const,
})
await client('https://example.com/data')
console.log(client.lastDurationMs)For advanced plugins, import public context types:
import type {
ClientPlugin,
PluginRequestContext,
PluginDispatch,
PluginSetupContext,
} from '@fetchkit/ffetch'What you can access in request context:
ctx.request: currentRequestobject.ctx.init: request init/options.ctx.state: per-request mutable plugin state.ctx.metadata: signal and retry metadata.
Use wrapDispatch when you need around-advice behavior (before/after dispatch in one place):
import type { ClientPlugin } from '@fetchkit/ffetch'
const tracingPlugin: ClientPlugin = {
name: 'tracing',
wrapDispatch: (next) => async (ctx) => {
console.log('start', ctx.request.url)
try {
const response = await next(ctx)
console.log('end', response.status)
return response
} catch (error) {
console.log('error', error)
throw error
}
},
}import { createClient } from '@fetchkit/ffetch'
const client = createClient({
timeout: 10_000,
retries: 2,
plugins: [
// custom plugin instances
],
})- Keep plugins side-effect free outside controlled state.
- Prefer per-request data in
ctx.stateinstead of global mutable variables. - Use
orderonly when needed; document ordering assumptions. - Avoid throwing from
onFinallyunless intentional. - Use
as constplugin tuples for best TypeScript extension inference.