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

@devmoods/fetch

v4.3.1

Published

JSON-friendly wrapper around the Fetch API with timeouts and automatic retries

Readme

@devmoods/fetch

Feature-rich and type-safe Fetch API wrapper with retries, timeouts, interceptors, and schema validation.

pnpm install @devmoods/fetch

Features

  • Auto-parse JSON responses into response.jsonData (no manual await response.json())
  • Create reusable fetch clients with shared defaults
  • Infer response types from generics or Standard Schema
  • Throw HttpError for non-success responses (status < 200 or status >= 400)
  • Request timeouts and configurable retry behavior
  • Request/response interceptors
  • Automatic request IDs (X-Request-ID) preserved across retries
  • Built-in snake_case / camelCase transformers
  • AbortSignal support across requests, retries, and timing helpers

Quick Start

import {
  HttpError,
  TimeoutError,
  createFetch,
  createRetryOn,
} from '@devmoods/fetch';

const fetch = createFetch({
  getRootUrl: () => 'http://localhost:3000/api',
  timeout: 2_000,
  retryOn: () =>
    createRetryOn({
      max: 3,
      isRetriable: (error) =>
        error instanceof TimeoutError ||
        (error instanceof HttpError && error.response.status === 503),
      getDelay: (attempt) => attempt * 500,
    }),
});

type User = { id: string; firstName: string };

const response = await fetch<User>('/users/1');
console.log(response.jsonData?.firstName);

Interceptors

fetch.intercept({
  request: (request) => {
    console.log('Request:', request.method, request.url);
  },
  response: (response) => {
    console.log('Response:', response.status, response.url);
  },
});

fetch.intercept(...) returns an unsubscribe function you can call to remove the interceptor.

Per-request Overrides

You can override client defaults for each request:

await fetch('/users', {
  method: 'POST',
  body: JSON.stringify({ firstName: 'Ada' }),
  timeout: 5_000,
  credentials: 'include',
  retryOn: () => false,
});

Transforms

Useful when your API returns snake_case but your app uses camelCase. Only response transforms are supported at the moment.

import { createFetch, snakeToCamelCase } from '@devmoods/fetch';

const fetch = createFetch({
  transform: snakeToCamelCase,
});

const response = await fetch('/users');
console.log(response.jsonData);

You can also provide a per-request transform:

await fetch('/users', {
  transform: {
    response: snakeToCamelCase,
  },
});

Schema Validation (Standard Schema)

Validation runs after transform, using any Standard Schema compatible library (for example ArkType or Zod).

import { type } from 'arktype';

const userSchema = type({ id: 'string', firstName: 'string' });

const response = await fetch('/users/1', {
  schema: userSchema,
});

response.jsonData; // typed as { id: string; firstName: string }

Retries with createRetryOn

createRetryOn helps define:

  • max attempts
  • which errors are retriable
  • retry delay strategy (linear, exponential, etc.)
import {
  HttpError,
  TimeoutError,
  createFetch,
  createRetryOn,
} from '@devmoods/fetch';

const fetch = createFetch({
  timeout: 2_000,
  retryOn: () =>
    createRetryOn({
      max: 5,
      isRetriable: (error) =>
        error instanceof TimeoutError ||
        (error instanceof HttpError && error.response.status === 503),
      getDelay: (attempt) => attempt * 500,
    }),
});

Signal Usage

Abort signals work with:

  • request cancellation (fetch('/path', { signal }))
  • retry delay cancellation (createRetryOn(...))
  • helper utilities (withTimeout, delay, timeout)

Cancel an in-flight request

const controller = new AbortController();

const promise = fetch('/users/1', { signal: controller.signal });

controller.abort(new Error('User navigated away'));
await promise; // rejects with the abort reason

Cancel retries from the same signal

When you pass signal to a request, default retry behavior also stops immediately when the signal aborts.

const controller = new AbortController();

const request = fetch('/unstable-endpoint', {
  signal: controller.signal,
});

setTimeout(() => controller.abort(new Error('Cancelled by UI')), 750);
await request;

Use signal-aware helper utilities

import { delay, withTimeout } from '@devmoods/fetch';

const controller = new AbortController();

await delay(1_000, { signal: controller.signal }); // rejects if aborted

await withTimeout(
  async (signal) => {
    // pass signal to nested operations
    await delay(200, { signal });
    return 'ok';
  },
  500,
  { signal: controller.signal },
);

Errors

  • HttpError: response status is outside expected success range
  • TimeoutError: request timed out
  • ValidationError: schema validation failed

License

MIT