Plugin System
Plugins extend Phtps without modifying the core. Every advanced feature — auth, retry, caching, encryption — is a plugin you opt into. The core client stays tiny and fast.
import { createHttpClient } from 'phtps';
import { AuthPlugin, RetryPlugin, CachePlugin } from 'phtps/plugins';
const client = createHttpClient({ baseURL: 'https://api.example.com' });
client.use(AuthPlugin());
client.use(RetryPlugin());
client.use(CachePlugin());Authentication
AuthPlugin
AuthenticationAutomatic token injection, 401 interception, and single-lock refresh.
import { AuthPlugin } from 'phtps/plugins';
client.use(
AuthPlugin({
getAccessToken: () => localStorage.getItem('token'),
onTokenRefresh: async () => {
const { token } = await refreshToken();
localStorage.setItem('token', token);
return token;
},
onRefreshFailure: (error) => {
localStorage.removeItem('token');
window.location.href = '/login';
},
})
);401 Refresh Flow
TokenRotationPlugin
AuthenticationProactively rotate tokens before they expire — no 401s needed.
Unlike AuthPlugin (which reacts to 401s), TokenRotationPlugin is proactive. It reads the token’s expiration, starts a background timer, and refreshes the token before it expires. All concurrent requests wait behind a single refresh lock.
import { AuthPlugin } from 'phtps/plugins';
client.use(
AuthPlugin({
tokenRotation: {
enabled: true,
getToken: () => localStorage.getItem('token') ?? undefined,
getExpiration: (token) => {
const { exp } = JSON.parse(atob(token.split('.')[1]));
return exp * 1000; // to ms epoch
},
onRefresh: async () => {
const { token } = await refreshToken();
localStorage.setItem('token', token);
return token;
},
refreshWindow: 60000, // refresh 60s before expiry
autoRefreshBackground: true, // start a background timer
headerName: 'Authorization',
headerPrefix: 'Bearer ',
},
})
);Reliability
RetryPlugin
ReliabilityExponential backoff with jitter. Never retries auth errors or cancellations.
import { RetryPlugin } from 'phtps/plugins';
client.use(
RetryPlugin({ retries: 3 })
);
// Per-request overrides
await client.get('/api/data', {
retries: 5,
retryDelay: 1000, // base delay in ms
retryDelay: (attempt) => attempt * 2000, // or a custom fn
retryCondition: (err) => err.status === 429, // custom predicate
});TimeoutPlugin
ReliabilityPer-request timeout with automatic AbortController cleanup.
Automatically aborts requests that exceed a time limit. Properly tears down the AbortController — no dangling promises or memory leaks.
// Set globally on the client
const client = createHttpClient({
timeout: 10_000, // 10 seconds (default)
});
// Override per request
const data = await client.get('/slow-endpoint', {
timeout: 30_000,
});
// Disable timeout for one request
await client.get('/long-poll', {
timeout: 0,
});Performance
CachePlugin
PerformanceTTL-based GET caching with swappable adapters. Memory, LocalStorage, or custom.
import { CachePlugin } from 'phtps/plugins';
// In-memory (default)
client.use(CachePlugin());
// Persistent across page loads
import { LocalStorageCacheAdapter } from 'phtps';
client.use(CachePlugin({
adapter: new LocalStorageCacheAdapter(),
}));
// Per-request
await client.get('/api/users', {
useCache: true,
cacheTTL: 300_000, // 5 minutes
});
// Invalidate manually
client.clearCache();QueuePlugin
PerformanceConcurrency control to prevent request storms and respect rate limits.
import { QueuePlugin } from 'phtps/plugins';
client.use(QueuePlugin({ concurrency: 3 }));
// Control at runtime
const queue = client.queueManager;
queue.pause(); // stop new tasks
queue.resume(); // resume
queue.setConcurrency(5); // change limit live
queue.clear(); // reject all queued tasks
// Bypass queue for one request
await client.get('/urgent', { queue: false });DedupPlugin
PerformanceCollapse identical in-flight GET requests into a single network call.
If three components call client.get('/user/me') at the same time, only one request leaves the browser. All three callers receive the same resolved value.
// Deduplication is ON by default for GET requests.
// No plugin needed — but you can control it:
// Disable for one request (force a fresh call)
await client.get('/api/user', { deduplicate: false });
// Explicitly install the plugin to dedup at middleware level
import { DedupePlugin } from 'phtps/plugins';
client.use(DedupePlugin());Security
EncryptionPlugin
SecurityEnd-to-end AES-GCM payload encryption using the native Web Crypto API.
import { EncryptionPlugin } from 'phtps/plugins';
client.use(
EncryptionPlugin({
secret: getUserKey(), // Inject at runtime — never hardcode
})
);
// Per-request
await client.post('/api/sensitive', { ssn: '123-45-6789' }, {
encryptionKey: runtimeKey,
encryptPayload: true,
decryptResponse: true,
});encryptionKey at runtime — from sessionStorage, a user passphrase, or a server-issued key. Never bundle it or put it in .env.CsrfPlugin
SecurityAuto-read CSRF tokens from cookies or meta tags and inject them on mutating requests.
Built into the core — no explicit plugin install needed. Works out of the box with Rails, Django, Laravel, and any custom CSRF implementation.
// Enable globally
const client = createHttpClient({
csrf: {
enabled: true,
cookieName: 'XSRF-TOKEN', // read from this cookie
headerName: 'X-XSRF-TOKEN', // send as this header
},
});
// Or configure per-request
await client.post('/api/action', data, {
csrf: {
enabled: true,
cookieName: 'csrf-token',
headerName: 'X-CSRF-Token',
methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
token: () => getCsrfToken(),
onTokenMissing: () => fetchNewCsrfToken(),
},
});PaymentPlugin
SecurityFeaturedPayment-grade security — HMAC signing, idempotency keys, PII masking, and replay protection.
This plugin is unusual — it is the only one designed specifically for financial APIs. It combines everything a payment integration needs into one install.
import { PaymentPlugin } from 'phtps/plugins';
const paymentClient = createHttpClient({
baseURL: 'https://payments.example.com',
});
paymentClient.use(PaymentPlugin({
environment: 'production',
secretKey: getRuntimeKey(), // HMAC signing secret
signRequests: true, // HMAC-SHA256 → X-Signature header
idempotency: true, // Auto Idempotency-Key per request
timestamp: true, // X-Timestamp replay prevention
maskSensitiveData: true, // Strip card / cvv / account from errors
rateLimit: {
maxRequests: 10,
windowMs: 60_000, // 10 req/min per process
},
}));
// Per-request: bring your own idempotency key
await paymentClient.post('/charge', payload, {
payment: {
idempotencyKey: 'order-abc-123',
skipRateLimit: true,
},
});Signs every request with HMAC-SHA256. Server can verify the payload hasn't been tampered with.
Auto-generates a UUID per request. Prevent duplicate charges if the network hiccups.
Strips card number, CVV, account numbers from any error thrown. PII never leaks into logs.
Injects X-Timestamp header. Servers can reject requests older than N seconds.
Data
PaginationPlugin
DataAuto-paginate any API — page, offset, or cursor — and merge all results automatically.
?page=1&pageSize=20?offset=20&limit=20?cursor=eyJpZCI6MTIzfQimport { PaginationPlugin } from 'phtps/plugins';
client.use([PaginationPlugin(), CachePlugin()]);
// Page-based — merges all pages into data[]
const { data } = await client.get('/api/posts', {
paginate: { strategy: 'page', limit: 100, pageSize: 20 },
});
// Cursor-based
const { data } = await client.get('/api/feed', {
paginate: { strategy: 'cursor', cursorField: 'nextCursor', limit: 200 },
});
// Offset-based
const { data } = await client.get('/api/logs', {
paginate: { strategy: 'offset', pageSize: 50, limit: 500 },
});
// Prefetch mode — silently loads next pages into cache
const { data } = await client.get('/api/posts', {
useCache: true,
paginate: {
mode: 'prefetch',
strategy: 'page',
limit: 3,
prefetchStrategy: 'idle', // 'immediate' | 'adaptive' | 'idle'
},
});Writing Custom Plugins
Any object with a name string and an install(client) function is a valid Phtps plugin. You get full access to interceptors, middleware, config, and the cache/queue managers.
function LoggerPlugin() {
return {
name: "logger",
install(client) {
client.interceptors.request.use(config => {
console.log(`→ ${config.method} ${config.url}`);
return config;
});
client.interceptors.response.use(
response => {
console.log(`← ${response.status} ${response.config.url}`);
return response;
},
error => {
console.error(`✗ ${error.response?.status} ${error.config?.url}`);
return Promise.reject(error);
}
);
}
};
}
// Usage
client.use(LoggerPlugin());What you can access inside install(client)
| Property / Method | Description |
|---|---|
| interceptors.request | Add request interceptors |
| interceptors.response | Add response interceptors |
| useMiddleware(fn) | Add to the middleware pipeline |
| setConfig(config) | Merge config into the client |
| request(config) | Make a request from inside the plugin |
| cacheManager | Access cache (if CachePlugin installed) |
| queueManager | Access queue (if QueuePlugin installed) |
| tokenRotationManager | Access proactive token rotation |
Plugin Order
ImportantThe order you call use() is the order plugins wrap the request. Think of it as an onion — the first plugin installed is the outermost layer.
client.use(AuthPlugin()); // 1. Outermost — handles tokens
client.use(CsrfPlugin()); // 2. Injects CSRF header
client.use(EncryptionPlugin()); // 3. Encrypts body close to wire
client.use(CachePlugin()); // 4. Caches successful responses
client.use(RetryPlugin()); // 5. Innermost — retries failed requestsRequest Pipeline
Response Pipeline
Why this order matters
Best Practices
Use Auth before Retry
Auth resolves tokens; Retry wraps the network call.
Use Encryption after Auth
Encrypt the final payload with all headers set.
Cache only GET requests
POST/PUT/DELETE are mutating — caching them causes stale data.
Avoid mutating config objects
Return a new config or call config.headers.set() — do not mutate.
Keep plugins stateless
Store state on the client (cacheManager, queueManager) not in closures.
Cleanup listeners on destroy
Remove interceptors and timers when client.destroy() is called.
Handle refresh failures
Always provide onRefreshFailure to redirect to login on hard auth failure.
Write plugin tests
Plugins are isolated — unit test them with a mock client.
