Phtps
/Features
19 features across 7 categories

Everything you need.
Nothing you don't.

A modular plugin architecture means every capability is opt-in. Add exactly what your app needs and ship a leaner bundle.

npm install @pings/phtps

Performance

Request Deduplication

Zero config

When 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

Pluggable

Built-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 friendly

Cap 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 backoff

Retries 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-request

Set 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

AbortController

Cancel 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 API

End-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-inject

Automatically 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 Phtps

A 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

Automatic

A 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 safe

Refreshes 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

Featured

Server-Sent Events (SSE)

OpenAI ready

First-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 APIs

Parse 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

ReadableStream

Access 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-in

Three 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

Composable

Middleware 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 ready

Plugins 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% typed

Every 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-compatible

Classic 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

Ready to ship faster?

All features ship in one package. Zero dependencies.