Skip to content

Repository files navigation

@questorylabs/qhttp

Fluent, extensible HTTP + WebSocket client for Node, browsers, and edge runtimes.

MIT · Node ≥ 20 · ESM + CJS


Table of contents


Install

pnpm add @questorylabs/qhttp

Optional peers:

pnpm add ws                  # Node WebSockets
pnpm add http-cache-semantics # RFC 9111 HTTP cache mode

Package entry points

Import Use for
@questorylabs/qhttp QHttp, cache engines, adapters, utilities
@questorylabs/qhttp/ws QWebSocket, browser/Node WS adapters
@questorylabs/qhttp/http2 Http2Adapter (Node only)
@questorylabs/qhttp/http-cache HTTP cache policy helpers
@questorylabs/qhttp/react ResourceStore, useResource, useAction, useLiveResource

React resource layer (@questorylabs/qhttp/react)

Framework-agnostic ResourceStore plus thin React bindings — not a TanStack Query clone.

import { ResourceProvider, useResource, useAction, useStore } from '@questorylabs/qhttp/react';

function App() {
  return (
    <ResourceProvider defaults={{ freshFor: 30_000, retries: 1 }}>
      <Dashboard />
    </ResourceProvider>
  );
}

function Dashboard() {
  const stats = useResource({
    id: ['dashboard'],
    load: () => fetch('/api/stats').then((r) => r.json()),
    refreshEvery: 30_000,
  });

  const save = useAction({
    run: (name: string) => fetch('/api/profile', { method: 'PATCH', body: name }),
    touches: [['me'], ['dashboard']],
  });

  if (stats.empty) return <p>Loading…</p>;
  return <pre>{JSON.stringify(stats.value)}</pre>;
}

Resource results expose value, empty, busy, refreshing, failed, ready, and reload(). Live SSE/WebSocket feeds use useLiveResource with an injected subscribe callback.


Quick start

Chainable — configure once, call many times:

import { QHttp } from '@questorylabs/qhttp';

const client = new QHttp({ baseUrl: 'https://api.example.com' })
  .setUrl('/users/{{userId}}')
  .replaceUrlMacros({ userId: 'u1' })
  .setQueryParams({ page: 1 })
  .setHeaders({ 'X-App': 'demo' })
  .setTimeout(5000)
  .cache()
  .cacheKey('users:{{userId}}')
  .preRequest((ctx) => {
    ctx.headers.set('x-timezone', 'UTC');
  })
  .postRequest(({ result }) => {
    result.data = { ...(result.data as object), transformed: true };
  });

const { data, httpStatus, fetchStatus } = await client.get();

Imperative — mutate between calls:

const req = new QHttp();
req.setBaseUrl('https://api.example.com');
req.setUrl('/recommendations');
if (userId) req.setQueryParams({ userId });

const result = await req.get();

WebSocket:

import { QWebSocket } from '@questorylabs/qhttp/ws';

const socket = new QWebSocket('wss://example.com/socket')
  .setReconnect({ retries: 5, delay: 1000, backoff: 'exponential' })
  .setHeartbeat({ intervalMs: 30000, message: 'ping', pongTimeoutMs: 5000 })
  .onMessage((msg) => console.log(msg.data))
  .connect();

socket.sendJson({ type: 'subscribe', channel: 'orders' });

Documentation

Full guides with examples, guidelines, and API notes live in [docs/](./docs/README.md).

Guide Topics
Getting started Concepts, fetchStatus, result shape, clone/reset/cancel
Requests URLs, macros, query params, body types, response parsing
Caching TTL + HTTP cache modes, engines, keys, cacheWhen
Hooks preRequest, postRequest, onError, custom phases
Retry & errors Backoff, jitter, Retry-After, QHttpError codes
Authentication Bearer and Basic auth
Adapters Fetch default, HTTP/2, custom transports
WebSockets Reconnect, heartbeat, Node ws adapter
API reference Method and type cheat sheet

Features

Area Highlights
API style Chainable builders + imperative setters on the same client
Lifecycle fetchStatus: idleloadingsuccess
URLs Base URL joining, query serialization, {{macro}} templates
Hooks preRequest, postRequest, onError, onRetry, preCache, postCache, custom phases
Retry Exponential/fixed backoff, jitter, Retry-After header, idempotent-method defaults
Cache TTL key/value store (~476k hits/s) + optional RFC 9111 HTTP mode via .httpCache()
Progress Upload + download via onProgress (FormData / streams included)
Transport Pluggable HttpAdapter; fetch by default, optional HTTP/2 on Node
WebSocket Reconnect, heartbeat, send queue, browser + Node adapters
Runtimes Node, browsers, edge — no framework lock-in

Limitations

  • No HTTP/3 adapter in core — use .setAdapter() for custom transports
  • Http2Adapter does not support FormData or ReadableStream request bodies (string / Buffer / ArrayBuffer / typed arrays / Blob / URLSearchParams only). Upload progress wrapping uses streams, so use the default fetch adapter when you need upload progress
  • FormData upload total is estimated from field sizes (not exact multipart byte length); opaque streams only get total when Content-Length is set
  • In-memory cache state does not persist across serverless cold starts
  • HTTP cache mode (.httpCache()) requires http-cache-semantics; TTL mode (.cache()) has no extra deps
  • HTTP cache stores one variant per key — use explicit keys when caching multiple Vary representations

License

MIT

About

A chainable, runtime-agnostic HTTP + WebSocket client with built-in caching, retries, and extensible request lifecycle hooks.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages