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

@boy-offi9-inc/reqkit

v0.1.7

Published

Small, composable HTTP helper functions — retry with backoff, timeouts, normalized errors, safe JSON parsing, rate-limit-aware retry, and download progress. Not a client, works alongside fetch/axios/anything.

Readme

reqkit

Small, composable HTTP helper functions. Not a client, not an axios replacement — works alongside fetch, axios, or anything else you're already using. Pull in the one function you need; ignore the rest.

const { withRetry, normalizeError } = require("@boy-offi9-inc/reqkit");

const data = await withRetry(() => fetch(url).then(r => r.json()));

Why this exists

Most HTTP retry/timeout wrappers bundle everything into a client you have to fully adopt. This is the opposite: standalone functions that compose with whatever you're already using, and a couple of small helpers that you can pick up independently.

Honest note: withRetry, withTimeout, and normalizeError solve a well-covered problem — there are other solid packages doing similar things (fetchy, fetchpilot, p-retry, to name a few).

  • retryAfterAware — actually reads Retry-After/X-RateLimit-Reset headers instead of blind backoff on 429s. Most retry libraries treat rate-limit responses like any other failure and ignore the server's suggested retry time.
  • withProgress — progress callback for any streamed response body (downloads, large payloads, anything), not tied to a specific HTTP client.
  • withDedupe — coalesces concurrent calls with the same key into one in-flight call, so simultaneous requests for the same resource don't each hit the network.

Zero dependencies. CJS + ESM. TypeScript types included.


Install

npm install @boy-offi9-inc/reqkit

API

withRetry(fn, opts?)

Retries an async function with exponential backoff + jitter.

const data = await withRetry(
  () => fetch(url).then(r => r.json()),
  { retries: 3, baseDelayMs: 300, onRetry: (err, attempt, delayMs) => console.log(`retry ${attempt} in ${delayMs}ms`) }
);

| Option | Default | Description | |---|---|---| | retries | 3 | max retry attempts after the first try | | baseDelayMs | 300 | initial delay | | maxDelayMs | 10000 | delay ceiling | | shouldRetry | retries anything but AbortError | (err, attempt) => boolean | | onRetry | — | (err, attempt, delayMs) => void | | signal | — | AbortSignal — cancels the whole retry loop, including any pending backoff wait, throwing RetryAbortedError |

// Cancel a long retry/backoff sequence externally — e.g. the user navigated away
const controller = new AbortController();
const promise = withRetry(() => fetch(url), { retries: 5, signal: controller.signal });
controller.abort(); // rejects with RetryAbortedError, even mid-backoff

withTimeout(fn, ms)

Races an async function against a timeout, passing it an AbortSignal so the underlying request can actually be cancelled.

const data = await withTimeout(
  signal => fetch(url, { signal }).then(r => r.json()),
  5000
);
// throws TimeoutError on timeout, regardless of whether fn cooperates with the signal

normalizeError(err)

One consistent error shape regardless of source (fetch, axios, node http, generic):

const { message, status, code, isNetworkError, isTimeout } = normalizeError(err);

Every field is always present (null/false when not applicable) — no existence checks needed.

parseJsonSafe(input)

Never throws on malformed JSON. Accepts a Response or a raw string.

const { data, error } = await parseJsonSafe(response);
if (error) { /* handle malformed JSON without a try/catch */ }

buildQuery(params)

Minimal query string builder — skips null/undefined, repeats the key for arrays, encodes everything.

buildQuery({ q: "hello world", tag: ["a", "b"] });
// "q=hello%20world&tag=a&tag=b"

For full parsing/nested-object support, use query-string instead — this only covers the common flat-object case with zero dependencies.

retryAfterAware(fn, opts?)

Retries a fetch-like call, honoring Retry-After/X-RateLimit-Reset headers on 429/503 instead of blind backoff.

const response = await retryAfterAware(
  () => fetch(url),
  { retries: 3, retryStatusCodes: [429, 503] }
);

| Option | Default | Description | |---|---|---| | retries | 3 | | | retryStatusCodes | [429, 503] | | | fallbackDelayMs | 1000 | used when no usable header is present | | maxDelayMs | 60000 | | | onRetry | — | (status, waitMs, attempt) => void |

withProgress(response, onProgress?)

Reads a streamed Response body while reporting progress, returns the collected bytes as a Uint8Array.

const bytes = await withProgress(response, ({ loaded, total, percent }) => {
  console.log(`${percent ?? "?"}% (${loaded}/${total ?? "?"} bytes)`);
});

Works without Content-Length too — total/percent are just null in that case, loaded is still accurate. Falls back to a single-shot read (still calling onProgress once) if the response body is not a stream.

withDedupe(fn, opts?)

Coalesces concurrent calls with the same arguments into a single in-flight call — every caller gets the same result, but the underlying request only fires once. Useful when several parts of a UI ask for the same resource around the same time.

const getUser = withDedupe((id) => fetch(`/api/users/${id}`).then(r => r.json()));

// Only one network request goes out — both callers share it.
const [a, b] = await Promise.all([getUser(1), getUser(1)]);

| Option | Default | Notes | | --- | --- | --- | | keyFn | (...args) => JSON.stringify(args) | derives the dedupe key from call args |

This is deduplication of concurrent calls, not a cache — once a call settles (success or failure), the next call with the same key starts fresh.

// Custom key when the default arg-based key isn't quite right
const fn = withDedupe((req) => doFetch(req), { keyFn: (req) => req.url });

Design notes

  • Zero dependencies. Nothing to audit, nothing to break underneath you.
  • CJS + ESM. require() and import both work.
  • Composable, not a client. Every function takes and returns plain values (Response, plain objects, Uint8Array) — nothing proprietary to learn.
  • TypeScript types included, hand-written (no build step to go wrong).