fn
definePlugin
v0.1.0Declares a typed fetch plugin. Identity function — returns its input verbatim at runtime, used only to narrow generics for strong option inference.
Examples
ts
<caption>Bearer token injection with typed per-request override</caption>
const auth = definePlugin<'auth', { token?: string }>({
name: 'auth',
hooks: {
onRequest: (ctx) => {
const token = (ctx.options as { token?: string }).token;
if (token !== undefined) ctx.options.headers.set('authorization', `Bearer ${token}`);
},
},
});
const api = createFetch({ plugins: [auth] });
await api('/me', { token: 'xyz' });ts
<caption>Auto-refresh on 401 using a shared factory closure</caption>
function createAuthPlugin(getAccessToken: () => Promise<string>) {
let current: Promise<string> | undefined;
const refresh = () => (current ??= getAccessToken().finally(() => { current = undefined; }));
return definePlugin<'auth', { skipAuth?: boolean }>({
name: 'auth',
hooks: {
onRequest: async (ctx) => {
if ((ctx.options as { skipAuth?: boolean }).skipAuth) return;
ctx.options.headers.set('authorization', `Bearer ${await refresh()}`);
},
onResponseError: async (ctx) => {
if (ctx.response.status !== 401) return;
// Invalidate cached token; next attempt via `retry` will pick up a fresh one.
current = undefined;
ctx.options.headers.set('authorization', `Bearer ${await refresh()}`);
},
},
defaults: { retry: 1, retryStatusCodes: [401, 408, 429, 500, 502, 503, 504] },
});
}ts
<caption>Idempotency-Key for unsafe methods</caption>
const idempotency = definePlugin<'idempotency', { idempotencyKey?: string }>({
name: 'idempotency',
hooks: {
onRequest: (ctx) => {
const method = (ctx.options.method ?? 'GET').toUpperCase();
if (method === 'GET' || method === 'HEAD') return;
const key = (ctx.options as { idempotencyKey?: string }).idempotencyKey ?? crypto.randomUUID();
ctx.options.headers.set('idempotency-key', key);
},
},
});ts
<caption>Response envelope unwrapping — { data, meta } → data</caption>
interface Envelope<T> { readonly data: T; readonly meta?: Record<string, unknown> }
const unwrap = definePlugin({
name: 'unwrap',
hooks: {
onResponse: (ctx) => {
const body = ctx.response._data as Envelope<unknown> | undefined;
if (body !== undefined && typeof body === 'object' && 'data' in body) {
ctx.response._data = body.data;
}
},
},
});ts
<caption>Timing + structured logger using WeakMap-keyed state</caption>
function createLoggerPlugin(sink: (record: { url: string; status: number; ms: number }) => void) {
const started = new WeakMap<object, number>();
return definePlugin({
name: 'logger',
hooks: {
onRequest: (ctx) => {
started.set(ctx, performance.now());
},
onResponse: (ctx) => {
const t = started.get(ctx);
if (t === undefined) return;
sink({ url: String(ctx.request), status: ctx.response.status, ms: performance.now() - t });
},
onResponseError: (ctx) => {
const t = started.get(ctx);
if (t === undefined) return;
sink({ url: String(ctx.request), status: ctx.response.status, ms: performance.now() - t });
},
},
});
}ts
<caption>Request ID / correlation header</caption>
const requestId = definePlugin<'requestId', { requestId?: string }>({
name: 'requestId',
hooks: {
onRequest: (ctx) => {
const id = (ctx.options as { requestId?: string }).requestId ?? crypto.randomUUID();
ctx.options.headers.set('x-request-id', id);
},
},
});ts
<caption>Composing multiple plugins — order matters</caption>
// Hooks execute in registration order, then any user per-request hook runs last.
// Here: requestId → auth → logger → user-provided onRequest.
const api = createFetch({
plugins: [requestId, createAuthPlugin(fetchToken), createLoggerPlugin(console.log), unwrap],
defaults: { baseURL: 'https://api.example.com' },
});
// Per-domain instance inherits every parent plugin and may add its own.
const billing = api.extend({ baseURL: 'https://billing.example.com' }, {
plugins: [idempotency],
});
await billing('/invoices', { method: 'POST', body: { amount: 100 } });Signature
ts
export function definePlugin<
const Name extends string,
OptionsExt = unknown,
ContextExt = unknown,
>(
plugin: FetchPlugin<Name, OptionsExt, ContextExt>,
): FetchPlugin<Name, OptionsExt, ContextExt>{ ... }Type Parameters
Nameextends stringOptionsExt= unknownContextExt= unknownParameters
| Parameter | Type | Description |
|---|---|---|
plugin | FetchPlugin<Name, OptionsExt, ContextExt> | — |
Returns
FetchPlugin<Name, OptionsExt, ContextExt>