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

@ultimatelemon-eu/fetch-client

v1.0.0

Published

fetch for rate-limited APIs: a token bucket that stays under the limit, Retry-After on 429, timeouts, one retry on 5xx, typed errors and logs without credentials.

Readme

@ultimatelemon-eu/fetch-client

fetch for APIs with a rate limit. It keeps you under the limit instead of finding out through 429s, waits when the API says so, gives up after a timeout, retries once when the upstream hiccups, and throws errors you can tell apart. No dependencies; runs anywhere fetch does.

npm install @ultimatelemon-eu/fetch-client
import { createFetchClient } from '@ultimatelemon-eu/fetch-client';

const ns = createFetchClient({
  baseUrl: 'https://gateway.apiportal.ns.nl',
  headers: { 'Ocp-Apim-Subscription-Key': process.env.NS_API_KEY ?? '' },
  // NS: 300 requests per 5 minutes per key.
  rateLimit: { limit: 300, windowMs: 300_000, burst: 40 }
});

const body = await ns.get<{ payload: unknown[] }>('/nsapp-stations/v3', {
  countryCodes: ['NL', 'BE'], // arrays repeat the parameter
  q: undefined // undefined and null are left out
});

Options

| Option | Default | Does | | ----------- | -------------- | ----------------------------------------------- | | baseUrl | required | Prefix for every path | | headers | {} | Sent with every request; never logged | | rateLimit | none | Token bucket, see below | | timeoutMs | 10_000 | Per attempt | | retries | 1 | Extra attempts after a 5xx or a network failure | | logger | silent | Anything with debug and warn, such as pino | | fetch | global fetch | For tests, or a runtime with its own |

Rate limiting

rateLimit: { limit, windowMs, burst, maxWaitMs? } sets up a token bucket that starts with burst tokens and refills at (limit - burst) / windowMs. Burst plus refill over any window never exceeds limit, however the upstream aligns its window, so a fixed-window API never sees request 301.

When the bucket is empty a request waits for the next token, up to maxWaitMs (default 5 s). Beyond that it throws RateLimitedError at once rather than hanging a caller for minutes.

On a 429 the client reads Retry-After (seconds or an HTTP date, 60 s when absent), blocks the bucket until then and throws RateLimitedError. Refill starts when the block ends, so the full burst does not land on the API the moment it lets you back in.

The bucket lives in the process: with several replicas, divide limit by the replica count.

Errors

| Error | When | Fields | | ------------------ | ----------------------------------------------------- | ------------------------ | | RateLimitedError | Bucket empty past maxWaitMs, or a 429 | retryAfterMs | | HttpError | Any other non-2xx, a timeout (504), unreachable (502) | status, path, body |

Timeouts are not retried: a request that took 10 s once will likely take 10 s again, and the caller is already waiting.

Logging

Each response is logged at debug with path, query, status and duration; failed attempts at warn. Headers are never logged, so a key in a header stays out of the logs. Keep credentials out of the query string.

Scope

Only get, which is what read-only API wrappers need. Writes would need a different retry policy (a POST is not safe to repeat), so they are left out until a project needs them.