npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sptzx/request

v1.0.0-rc.1

Published

Tiny, Elegant, and Brutally Optimized HTTP Client for Server-Side Runtimes. Zero-dependency.

Readme

@sptzx/request

A zero-dependency HTTP client for server-side runtimes — built on top of the native Fetch API.

npm version license zero dependencies Node.js Bun Deno


@sptzx/request is a production-grade HTTP client that wraps the native fetch API with everything server-side code actually needs: smart retries, automatic cookie persistence, circuit breaking, NDJSON streaming, proxy support, and lifecycle hooks — all with zero external dependencies.

It is built for Node.js ≥ 18, Bun, and Deno, and ships as fully typed TypeScript source alongside a compiled dist/.


Table of Contents


✨ Features

| Feature | Description | |---|---| | Zero Dependencies | No node_modules at runtime. Ever. | | Smart Retry | Exponential backoff, per-status retry rules, Retry-After header support, and jitter | | Cookie Jar | RFC 6265-compliant automatic cookie management across requests and redirects | | Circuit Breaker | CLOSED → OPEN → HALF_OPEN state machine to protect upstream services | | Smart Redirects | Native redirect for speed; manual redirect when a CookieJar is active for correctness | | NDJSON Streaming | First-class AsyncGenerator for newline-delimited JSON (LLMs, event streams) | | Lifecycle Hooks | beforeRequest, beforeRetry, afterResponse, beforeError | | Progress Callbacks | Streaming upload and download progress | | Proxy Support | proxyUrl shorthand or raw Undici dispatcher | | OOM Protection | Error body reads capped at 10 MB, racing against active timeout | | Full TypeScript | 100% typed — every option, hook, error, and return value |


📦 Installation

# npm
npm install @sptzx/request

# yarn
yarn add @sptzx/request

# pnpm
pnpm add @sptzx/request

# bun
bun add @sptzx/request

Runtime requirements: Node.js ≥ 18.0.0, Bun (any version), or Deno (any version).


⚡ Quick Start

import request from '@sptzx/request';

// GET and parse JSON
const users = await request.get('https://api.example.com/users').json();

// POST with JSON body
const created = await request.post('https://api.example.com/users', {
  json: { name: 'Alice', role: 'admin' },
}).json();

// Automatic retry on failure (default: 2 retries)
const data = await request.get('https://api.example.com/data', {
  retry: 3,
  timeout: 5_000,
}).json();

🚀 Usage

Basic Requests

import request from '@sptzx/request';

// GET
const data = await request.get('https://api.example.com/users').json<User[]>();

// GET with query parameters
const results = await request.get('https://api.example.com/search', {
  searchParams: { q: 'nodejs', page: '1', limit: '20' },
}).json();

// POST — JSON body
const user = await request.post('https://api.example.com/users', {
  json: { name: 'Alice', role: 'admin' },
}).json<User>();

// PUT / PATCH / DELETE
await request.put('https://api.example.com/users/1', { json: { name: 'Bob' } });
await request.patch('https://api.example.com/users/1', { json: { role: 'viewer' } });
await request.delete('https://api.example.com/users/1');

// Access the raw Response
const response = await request.get('https://api.example.com/health');
console.log(response.status);                          // 200
console.log(response.headers.get('x-request-id'));

// Other body types
const text   = await request.get('https://api.example.com/report').text();
const buffer = await request.get('https://files.example.com/file.bin').arrayBuffer();
const blob   = await request.get('https://files.example.com/image.png').blob();

Cookie Jar (Session Management)

@sptzx/request includes a built-in RFC 6265-compliant CookieJar. When attached to an instance, it automatically stores Set-Cookie response headers and injects the correct Cookie header on all subsequent requests — including across redirects and retries.

import request, { CookieJar } from '@sptzx/request';

const jar = new CookieJar();
const session = request.extend({ cookieJar: jar });

// Login — the response's Set-Cookie is stored automatically
await session.post('https://api.example.com/auth/login', {
  json: { username: 'alice', password: 's3cr3t' },
});

// All subsequent requests send the stored cookie automatically
const profile = await session.get('https://api.example.com/me').json();
const orders  = await session.get('https://api.example.com/orders').json();

// Logout — jar is cleared when the session cookie expires
await session.post('https://api.example.com/auth/logout');

Retry & Circuit Breaker

import request, { CircuitBreaker } from '@sptzx/request';

const breaker = new CircuitBreaker({
  threshold:        5,      // Open after 5 consecutive failures
  halfOpenAfterMs:  15_000, // Try again after 15 seconds
  successThreshold: 2,      // Close again after 2 successful probes
  onStateChange: (from, to) => console.log(`Circuit: ${from} → ${to}`),
});

const api = request.extend({
  prefixUrl:      'https://api.example.com',
  timeout:        8_000,
  circuitBreaker: breaker,
  retry: {
    limit:            3,
    methods:          ['get', 'post', 'put'],
    statusCodes:      [408, 429, 502, 503, 504],
    afterStatusCodes: [429],          // Respect Retry-After header on 429
    backoffLimit:     30_000,         // Cap individual delay at 30 s
    jitter:           true,           // Randomise delay to avoid thundering-herd
    delay: (attempt) => 300 * 2 ** (attempt - 1), // 300ms, 600ms, 1200ms...
  },
  hooks: {
    beforeRequest: [
      ({ request }) => {
        request.headers.set('Authorization', `Bearer ${process.env.API_TOKEN}`);
      },
    ],
    beforeRetry: [
      ({ error, retryCount }) => {
        console.warn(`[retry #${retryCount}] ${error.message}`);
      },
    ],
  },
});

const data = await api.get('users').json();

Smart Redirects

@sptzx/request automatically selects the right redirect strategy based on your configuration:

  • Native redirect (default) — when no cookieJar is active, redirects are delegated entirely to the runtime's native fetch. Zero overhead.
  • Manual redirect (with cookieJar) — when a cookieJar is active, every redirect hop is intercepted in userland. This is required because native fetch silently drops Set-Cookie headers on intermediate hops, which would corrupt session state.

During manual redirect, @sptzx/request:

  1. Persists Set-Cookie from each intermediate response to the jar.
  2. Injects the correct Cookie header for the next hop URL.
  3. Rewrites POSTGET on 303 See Other (per RFC 7231).
  4. Preserves the method on 307 Temporary Redirect and 308 Permanent Redirect.
  5. Throws if the number of hops exceeds maxRedirects (default: 10).
import request, { CookieJar } from '@sptzx/request';

const jar     = new CookieJar();
const session = request.extend({ cookieJar: jar });

// Cookies from 302 intermediate hops are captured automatically
await session.get('https://api.example.com/login');

// Limit redirect depth
await session.get('https://api.example.com/page', { maxRedirects: 5 });

NDJSON Streaming

Ideal for consuming LLM token streams, server-sent database diffs, or any newline-delimited JSON endpoint.

import request from '@sptzx/request';

const stream = request.post('https://api.example.com/llm/completions', {
  json: { model: 'gpt-4', prompt: 'Hello', stream: true },
  timeout: false, // Disable timeout for long-running streams
});

for await (const chunk of stream.ndjson<{ token: string; done: boolean }>()) {
  process.stdout.write(chunk.token);
  if (chunk.done) break;
}

Instance Configuration

Use extend() to create pre-configured instances with shared defaults. Instances inherit and deep-merge from their parent — ideal for building service-specific clients once and reusing them everywhere.

import request from '@sptzx/request';

// Base HTTP client
const http = request.extend({
  timeout: 10_000,
  retry:   { limit: 2 },
  headers: { 'User-Agent': 'MyApp/2.0' },
});

// GitHub API client (extends http)
const github = http.extend((parent) => ({
  ...parent,
  prefixUrl: 'https://api.github.com/',
  headers: {
    ...parent.headers,
    Authorization: `token ${process.env.GITHUB_TOKEN}`,
    Accept: 'application/vnd.github+json',
  },
}));

// Stripe API client (extends http)
const stripe = http.extend({
  prefixUrl: 'https://api.stripe.com/v1/',
  headers: {
    Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
  },
});

const repos   = await github.get('user/repos').json();
const charges = await stripe.get('charges').json();

Upload & Download Progress

import request from '@sptzx/request';

// Download with progress
const response = await request.get('https://files.example.com/dataset.zip', {
  onDownloadProgress: ({ percent, transferredBytes, totalBytes }) => {
    process.stdout.write(`\rDownloading: ${Math.round(percent * 100)}%`);
  },
});
const buffer = await response.arrayBuffer();

// Upload with progress
await request.post('https://api.example.com/upload', {
  body: fileStream,
  onUploadProgress: ({ percent, transferredBytes }) => {
    process.stdout.write(`\rUploading: ${Math.round(percent * 100)}%`);
  },
});

Error Handling

import request, {
  HTTPError,
  TimeoutError,
  CircuitBreakerOpenError,
  isRequestError,
} from '@sptzx/request';

try {
  const data = await request.get('https://api.example.com/resource').json();
} catch (error) {
  if (error instanceof HTTPError) {
    // error.response — the raw Response object
    // error.data     — parsed response body (JSON or text)
    console.error(`HTTP ${error.response.status}:`, error.data);

  } else if (error instanceof TimeoutError) {
    console.error('Request timed out after', error.request.url);

  } else if (error instanceof CircuitBreakerOpenError) {
    console.error('Circuit breaker is OPEN — request skipped');

  } else if (isRequestError(error)) {
    // Catches any @sptzx/request error (umbrella guard)
    console.error('Request error:', error.message);

  } else {
    throw error; // Re-throw unrecognised errors
  }
}

To disable automatic error throwing and handle responses manually:

const response = await request.get('https://api.example.com/resource', {
  throwHttpErrors: false,
});

if (!response.ok) {
  const body = await response.json();
  console.error(`Error ${response.status}:`, body);
}

🗂️ API Reference

HTTP Methods

request(url, options?)          // Generic request
request.get(url, options?)
request.post(url, options?)
request.put(url, options?)
request.patch(url, options?)
request.delete(url, options?)
request.head(url, options?)

All methods return a ResponsePromise — a native Promise<Response> extended with body helpers:

| Method | Returns | |---|---| | .json<T>() | Promise<T> | | .text() | Promise<string> | | .blob() | Promise<Blob> | | .arrayBuffer() | Promise<ArrayBuffer> | | .formData() | Promise<FormData> | | .bytes() | Promise<Uint8Array> | | .ndjson<T>() | AsyncGenerator<T> |

Instance Methods

| Method | Description | |---|---| | request.extend(defaults \| fn) | Create a new instance that inherits and merges from the current instance | | request.create(defaults) | Create a new instance with only the given defaults (no inheritance) |

Options

| Option | Type | Default | Description | |---|---|---|---| | prefixUrl | string \| URL | '' | Base URL prepended to every request path | | method | string | 'GET' | HTTP method | | headers | HeadersInit | {} | Request headers | | json | unknown | — | JSON body — auto-sets Content-Type: application/json | | searchParams | string \| Record \| URLSearchParams | — | Query string parameters | | timeout | number \| false | 10000 | Wall-clock timeout (ms) for the entire operation including retries | | retry | RetryOptions \| number | 2 | Retry limit or full retry configuration | | cookieJar | CookieJar | — | Enables automatic RFC 6265 cookie management | | circuitBreaker | CircuitBreaker | — | Circuit breaker instance | | throwHttpErrors | boolean \| (status) => boolean | true | Throw HTTPError on non-2xx responses | | redirect | 'follow' \| 'manual' \| 'error' | 'follow' | Redirect mode — overridden to 'manual' automatically when cookieJar is active | | maxRedirects | number | 10 | Maximum redirect hops (applies when cookieJar is active) | | proxyUrl | string | — | HTTP/HTTPS proxy URL | | dispatcher | Dispatcher | — | Raw Undici dispatcher (Pool, ProxyAgent, etc.) | | hooks | Hooks | {} | Lifecycle hooks object | | onDownloadProgress | (progress, chunk) => void | — | Called during response body streaming | | onUploadProgress | (progress, chunk) => void | — | Called during request body streaming | | parseJson | (text) => unknown | JSON.parse | Custom JSON deserializer | | stringifyJson | (value) => string | JSON.stringify | Custom JSON serializer | | context | Record<string, unknown> | {} | Arbitrary metadata passed through to all hooks | | fetch | typeof fetch | globalThis.fetch | Custom fetch implementation |

Retry Options

| Option | Type | Default | Description | |---|---|---|---| | limit | number | 2 | Maximum number of retry attempts | | methods | string[] | ['get','put','head','delete'] | Methods eligible for retry | | statusCodes | number[] | [408,413,429,500,502,503,504,521,522,524] | Status codes that trigger a retry | | afterStatusCodes | number[] | [413,429,503] | Status codes that trigger Retry-After header parsing | | maxRetryAfter | number | undefined | Maximum ms to wait when honouring Retry-After | | backoffLimit | number | Infinity | Maximum ms for any single retry delay | | delay | (attempt) => number | Exponential | Custom delay function | | jitter | boolean \| (delay) => number | false | Randomise delay to reduce thundering-herd | | retryOnTimeout | boolean | false | Retry on TimeoutError |

Error Classes

| Class | Description | |---|---| | HTTPError | Thrown on non-2xx responses. Has .response, .request, .options, .data | | TimeoutError | Thrown when the wall-clock timeout expires | | ForceRetryError | Thrown inside hooks to force an immediate retry | | CircuitBreakerOpenError | Thrown when the circuit breaker is in OPEN state |

Type Guards

import {
  isRequestError,           // HTTPError | TimeoutError | ForceRetryError | CircuitBreakerOpenError
  isHTTPError,              // HTTPError
  isTimeoutError,           // TimeoutError
  isForceRetryError,        // ForceRetryError
  isCircuitBreakerOpenError // CircuitBreakerOpenError
} from '@sptzx/request';

🙏 Credits

@sptzx/request was built by consolidating and extending the work of these open-source projects:

  • sindresorhus/ky (MIT © Sindre Sorhus) — The architecture, retry logic, hook system, and ResponsePromise pattern that form the structural foundation of this library.
  • salesforce/tough-cookie (BSD-3-Clause © Salesforce) — The RFC 6265 cookie algorithms powering domain matching, path matching, and attribute parsing.
  • nfriedly/set-cookie-parser (MIT © Nathaniel Friedman) — The Set-Cookie header splitting and parsing logic used in cookie/parser.ts.

📜 License

MIT@sptzx/request core and Ky-derived portions. BSD-3-Clause — tough-cookie-derived portions.

See LICENSE for the full license texts.