Performance
Request Deduplication
Zero configWhen multiple callers fire the same GET request simultaneously, Phtps merges them into a single in-flight promise. Every caller gets the same resolved value — only one network round-trip happens.
// Three callers, one network request
const [a, b, c] = await Promise.all([
client.get('/user/me'),
client.get('/user/me'),
client.get('/user/me'),
]);
// → Identical. Only one request was sent.- Race-condition safe by default
- Works across components and hooks
- No configuration required
- Disableable per-request
Caching
PluggableBuilt-in TTL-based GET caching with swappable storage adapters. Cache in memory during a session, in LocalStorage across page loads, or bring your own backend.
const client = phtps.create({
cache: true,
cacheTTL: 60_000, // 1 minute
});
// Custom adapter (e.g. Redis)
const client = phtps.create({
cache: new RedisCacheAdapter({ ttl: 300 }),
});- Memory cache (default)
- LocalStorage adapter
- SessionStorage adapter
- Custom adapter interface
- Per-request TTL override
Queue & Concurrency
Rate-limit friendlyCap the number of simultaneous requests to stay within API rate limits, protect servers from bursts, or manage file upload queues without throttling logic in your app code.
const client = phtps.create({
maxConcurrentRequests: 5,
queueStrategy: 'fifo', // or 'lifo'
});
// Pause and resume the queue at any time
client.queue.pause();
client.queue.resume();- FIFO and LIFO strategies
- Pause / resume queue
- Per-client or global limits
- Works with file uploads and batch APIs
Reliability
Automatic Retry
Exponential backoffRetries transient failures automatically with configurable exponential backoff and jitter. Define exactly which errors should trigger a retry so you never retry client mistakes.
const client = phtps.create({
retry: 3, // max attempts
retryDelay: 'exponential', // or a custom fn
retryCondition: (err) =>
err.status >= 500 ||
err.code === 'NETWORK_ERR',
});- Exponential backoff with jitter
- Custom retry predicates
- Per-request override
- 5xx and network error defaults
- Retry-After header support
Timeout
Per-requestSet a global timeout on the client and override it per request. Phtps cleans up correctly — no dangling promises, no memory leaks.
// Global default
const client = phtps.create({ timeout: 10_000 });
// Override per request
const data = await client.get('/slow-endpoint', {
timeout: 30_000,
});- Global and per-request timeout
- Automatic AbortController cleanup
- No dangling promises
Request Cancellation
AbortControllerCancel any in-flight request — including queued ones — using the standard AbortController API. Integrates directly with React's useEffect cleanup and TanStack Query.
const controller = new AbortController();
client.get('/stream', {
signal: controller.signal,
});
// Cancel from anywhere
controller.abort();
// React: cancel on unmount
useEffect(() => {
const ctrl = new AbortController();
client.get('/data', { signal: ctrl.signal });
return () => ctrl.abort();
}, []);- Standard AbortController API
- Cancels queued requests too
- React useEffect friendly
- Works with TanStack Query
Security
Payload Encryption
Web Crypto APIEnd-to-end AES-GCM payload encryption using the native Web Crypto API — no third-party crypto dependencies. Payloads are encrypted before leaving the browser and decrypted on the server.
import { EncryptionPlugin } from 'phtps/plugins';
const client = phtps.create({
plugins: [
EncryptionPlugin({
key: process.env.CLIENT_SECRET_KEY,
algorithm: 'AES-GCM',
}),
],
});
// Request body is encrypted automatically
await client.post('/vault', { secret: 'data' });- AES-GCM with random IV per request
- PBKDF2 key derivation
- Key caching for performance
- Zero extra dependencies
- Server-side decryption helpers included
CSRF Protection
Auto-injectAutomatically reads CSRF tokens from cookies or meta tags and injects them into every mutating request. Works with Rails, Django, Laravel, and any custom CSRF implementation.
import { CsrfPlugin } from 'phtps/plugins';
const client = phtps.create({
plugins: [
CsrfPlugin({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN',
}),
],
});
// Token is injected on POST, PUT, PATCH, DELETE- Cookie and meta-tag extraction
- Double-submit cookie pattern
- Customisable header name
- Framework presets (Rails, Django, Laravel)
Payment Security
Unique to PhtpsA purpose-built plugin for payment APIs that combines HMAC request signing, automatic idempotency keys, and PII masking in request logs — the full trifecta expected by Stripe, Razorpay, and PayPal.
import { PaymentPlugin } from 'phtps/plugins';
const client = phtps.create({
plugins: [
PaymentPlugin({
hmacSecret: process.env.PAYMENT_SECRET,
idempotency: true, // auto-generate keys
maskFields: ['card', 'cvv', 'ssn'],
}),
],
});- HMAC-SHA256 request signing
- Automatic idempotency key generation
- PII field masking in logs
- Stripe, Razorpay, PayPal presets
- Replay-attack protection
Authentication
Auth Plugin
AutomaticA drop-in authentication plugin that attaches tokens to every request, detects 401 responses, and refreshes the token — all without you writing a single interceptor.
import { AuthPlugin } from 'phtps/plugins';
const client = phtps.create({
plugins: [
AuthPlugin({
getToken: () => localStorage.getItem('token'),
refreshToken: async () => {
const res = await fetch('/auth/refresh');
return res.json().then(r => r.token);
},
}),
],
});- Bearer token injection
- Reactive 401 refresh
- Proactive pre-expiry refresh window
- Customisable token storage
Token Rotation
Race-condition safeRefreshes access tokens proactively before they expire. If multiple requests trigger a refresh simultaneously, only one refresh call is made — all others wait and get the new token automatically.
AuthPlugin({
refreshWindow: 60, // seconds before expiry
onRefresh: (token) => {
store.dispatch(setToken(token));
},
onRefreshFail: () => {
router.push('/login');
},
})- Proactive refresh window (configurable)
- Single refresh lock — no stampede
- Queues concurrent requests during refresh
- onRefreshFail callback for logout flows
Streaming
Server-Sent Events (SSE)
OpenAI readyFirst-class SSE support with automatic reconnection, event filtering, and a clean async-iterator API. No EventSource wrapper gymnastics — just stream and iterate.
const stream = await client.stream('/ai/chat', {
method: 'POST',
body: { prompt: 'Hello' },
streamType: 'sse',
});
for await (const event of stream) {
console.log(event.data); // streamed tokens
}- Async iterator API
- Automatic reconnection
- Event-type filtering
- Works with OpenAI, Anthropic, Gemini
- POST body support (unlike native EventSource)
NDJSON Streaming
Real-time APIsParse newline-delimited JSON as it arrives over the wire. Each line is parsed and emitted as a typed object — no buffering, no manual split logic.
const stream = await client.stream('/events', {
streamType: 'ndjson',
});
for await (const event of stream) {
// event is already parsed JSON
updateUI(event);
}- Zero-buffer line splitting
- Auto JSON.parse per line
- Type-safe with generics
- Docker logs, K8s events, LLM APIs
Raw Byte Stream
ReadableStreamAccess the raw ReadableStream directly for maximum control — pipe to a WritableStream, transform with TransformStream, or hand off to the File System Access API.
const { body } = await client.stream('/download', {
streamType: 'raw',
});
// Pipe directly to a file
await body.pipeTo(
new WritableStream({ write(chunk) { ... } })
);- Direct ReadableStream access
- Pipe to WritableStream
- Compose with TransformStream
- File System Access API compatible
Pagination
Pagination Strategies
3 built-inThree pagination strategies out of the box — page-based, offset-based, and cursor-based. Switch strategies without changing your consumer code.
// Page strategy → ?page=2&pageSize=20
// Offset strategy → ?offset=40&limit=20
// Cursor strategy → ?after=eyJpZCI6MTIzfQ
const paginator = client.paginate('/posts', {
strategy: 'cursor',
pageSize: 20,
});
for await (const page of paginator) {
renderPosts(page.data);
}- Page, offset, and cursor strategies
- Async iterator over pages
- Auto-fetch all pages with .collect()
- Prefetch next page in background
- Max-page guard to prevent runaway loops
Developer Experience
Koa-style Middleware
ComposableMiddleware runs in an onion model — code before `await next()` runs on the request, code after runs on the response. Compose multiple middleware for logging, tracing, mutation, and more.
client.use(async (ctx, next) => {
const start = Date.now();
ctx.request.headers.set('X-Request-Id', uuid());
await next(); // ← request fires here
const ms = Date.now() - start;
ctx.response.headers.set('X-Timing', `${ms}ms`);
logger.info(`${ctx.request.method} ${ms}ms`);
});- Koa-style onion model
- Full request and response mutation
- Async/await native
- Composable — stack as many as you need
Plugin System
Ecosystem readyPlugins are composable middleware bundles. Use official plugins or publish your own. Each plugin is a factory function that returns a middleware — nothing magic, nothing opaque.
import { RetryPlugin, AuthPlugin, CachePlugin } from 'phtps/plugins';
const client = phtps.create({
plugins: [
RetryPlugin({ retries: 3 }),
AuthPlugin({ getToken }),
CachePlugin({ ttl: 60_000 }),
],
});
// Or add dynamically
client.use(LogPlugin({ level: 'debug' }));- Official plugin registry
- Zero-friction authoring API
- Tree-shakeable — import only what you use
- Plugins compose with middleware
TypeScript-First
100% typedEvery API surface is fully typed with generics, discriminated unions, and precise overloads. Response types flow through plugins and middleware without losing information.
interface User { id: string; name: string }
// Generic response type
const { data } = await client.get<User>('/me');
// ^ data is User ✓
// Typed plugin options
client.use(RetryPlugin<MyConfig>({ ... }));
// Stream type preserved
const stream = client.stream<ChatEvent>('/ai');- Full generic inference
- Plugin types preserved end-to-end
- No any casts needed
- ships its own .d.ts — no @types package
Interceptors
Axios-compatibleClassic request and response interceptors for developers migrating from Axios. They run outside the middleware pipeline — perfect for global error handling and auth injection.
// Request interceptor
client.interceptors.request.use((config) => {
config.headers['X-App-Version'] = APP_VERSION;
return config;
});
// Response interceptor
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.status === 401) logout();
return Promise.reject(error);
}
);- Axios-compatible API
- Request and response interceptors
- Eject interceptors by ID
- Coexists with middleware pipeline
