Creating Plugins
axios-retryer should grow through plugins, not through one-off flags in the core. Use a single plugin architecture across the repo so every plugin stays predictable to read, test, and extend.
Keep the top-level plugin class as an orchestrator. Put public contracts in types/,
defaults and validation in configs/, errors in errors/,
pure helpers in utils/, and stateful collaborators in managers/,
storage/, or interceptors/ only when needed.
When to create a plugin
- Create a plugin when the feature is optional, cross-cutting, or can be expressed through events and request lifecycle hooks.
- Do not add niche feature flags to
RetryManagerOptionswhen the behavior belongs in a plugin. - Keep the core focused on orchestration and shared infrastructure.
Single recommended folder structure
Use this as the default shape for new plugins. Omit folders you do not need, but do not invent a new structure for each plugin.
src/plugins/YourPlugin/
YourPlugin.ts
index.ts
types/
index.ts
configs/
index.ts
errors/
YourPluginError.ts
index.ts
utils/
*.ts
index.ts
managers/
*.ts
storage/
*.ts
interceptors/
RequestInterceptor.ts
ResponseInterceptor.ts
ErrorInterceptor.ts Responsibility split
YourPlugin.ts: lifecycle wiring, interceptor registration, event emission, public methods, teardown.index.ts: public exports and the optionalcreateYourPlugin(...)factory only.types/: options, events, adapter contracts, state records, request metadata types.configs/: defaults, normalization, and validation.errors/: plugin-specific error classes.utils/: pure helpers with explicit return types and no shared mutable state.managers/orstorage/: stateful subsystems with narrow interfaces.interceptors/: request/response/error phase boundaries when lifecycle logic becomes substantial.
Minimal plugin skeleton
import type { PluginContext, RetryPlugin } from 'axios-retryer';
export interface YourPluginEvents {
onSomethingHappened?: (payload: { requestId: string }) => void;
}
export interface YourPluginOptions {
enabled?: boolean;
}
export class YourPlugin implements RetryPlugin<YourPluginEvents> {
public name = 'YourPlugin';
public version = '1.0.0';
public readonly _events?: Readonly<YourPluginEvents>;
private context!: PluginContext<YourPluginEvents>;
constructor(private readonly options: Required<YourPluginOptions>) {}
public initialize(context: PluginContext<YourPluginEvents>): void {
this.context = context;
}
public onBeforeDestroyed(): void {
// Clean up listeners, timers, interceptors, and state here.
}
} Current plugin contract
New plugins should target the current RetryPlugin<TPluginEvents> interface from
src/types/plugins.ts. The _events marker is important because it preserves
event inference when users call manager.use(plugin).
export interface RetryPlugin<TPluginEvents extends object = {}> {
name: string;
version: string;
readonly _events?: Readonly<TPluginEvents>;
initialize: (context: PluginContext<TPluginEvents>) => void;
onBeforeDestroyed?: (context: PluginContext<TPluginEvents>) => void;
} Current PluginContext contract
initialize(context) receives the full plugin-facing manager bridge. Design plugin behavior
around this existing surface before adding new APIs.
export interface PluginContext<TPluginEvents extends object = {}> {
readonly axiosInstance: AxiosInstance;
getLogger(): Logger;
on(...): void;
off(...): boolean;
emit(...): void;
triggerAndEmit(...): void;
cancelRequest(requestId: string): void;
cancelAllRequests(): void;
cancelQueuedRequests(): void;
registerQueueGate(name: string, canProcess: (request: AxiosRequestConfig) => boolean): void;
unregisterQueueGate(name: string): boolean;
refreshQueue(): void;
registerMetricsRecorder(recorder: MetricsRecorder | null): void;
getTimerStats(): { activeTimers: number; activeRetryTimers: number };
releaseRequestTracking(config: AxiosRequestConfig): void;
} - Use
axiosInstanceto register Axios interceptors owned by the plugin. - Use
on,off,emit, andtriggerAndEmitfor event-driven behavior. - Use queue APIs such as
registerQueueGateandrefreshQueuefor gating or coordination. - Use
registerMetricsRecorderonly when the plugin is responsible for manager-visible metrics. - Use
releaseRequestTrackingonly for advanced request lifecycle ownership, such as refresh/replay flows.
Public entrypoint shape
The package entrypoint should stay small and predictable. Export the class, public errors,
public types, and optional factory function from index.ts.
export { YourPlugin } from './YourPlugin';
export { YourPluginError } from './errors';
export type { YourPluginEvents, YourPluginOptions } from './types';
import { YourPlugin, type YourPluginOptions } from './YourPlugin';
export function createYourPlugin(options?: YourPluginOptions): YourPlugin {
return new YourPlugin(options);
} Best practices
- Emit plugin behavior at the outer boundary with
context.triggerAndEmit(...)instead of coupling the core to plugin internals. - Keep side effects near boundaries and keep helper logic deterministic.
- Use explicit return types and never use
any. - If a file grows beyond roughly 200 lines, extract a focused collaborator.
- Split request, response, and error handling when a plugin starts mixing lifecycle phases.
- Write focused tests for config validation, side effects, and plugin-specific state transitions.
Suggested workflow
- Read the plugin contract in
src/types/plugins.ts. - Map the feature to the existing
PluginContextmethods before adding implementation details. - Inspect an existing plugin before inventing a new shape.
- Define public events and options first.
- Implement defaults and validation in
configs/. - Keep the plugin class focused on orchestration.
- Extract pure logic and stateful boundaries as soon as they stop being trivial.
- Document the plugin on this website when its public behavior is user-facing.