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

@jeppech/typed-fetcher

v2.1.0

Published

A fetch class, for typing HTTP endpoints

Readme

@jeppech/typed-fetcher

A typed fetch wrapper for TypeScript.

Define your API once, then get typed routes, request builders, middleware, concurrency control, and typed response parsing.

Install

pnpm add @jeppech/typed-fetcher

Quick Start

import { TypedFetcher, define_endpoints, endpoint } from '@jeppech/typed-fetcher';

type UnitCreated = {
  id: string;
};

type UnitItem = {
  id: string;
  name: string;
};

type HttpOkBody = {
  ok: true;
};

type HttpErrorBody = {
  message: string;
};

const endpoints = define_endpoints({
  '/account/unit': {
    post: endpoint<{ ok: UnitCreated; err: HttpErrorBody }>(),
  },
  '/account/unit/:unit_id': {
    get: endpoint<{ ok: UnitItem; err: HttpErrorBody }>(),
    put: endpoint<{ ok: UnitItem; err: HttpErrorBody }>(),
    delete: endpoint<{ ok: HttpOkBody; err: HttpErrorBody }>(),
  },
});

const fetcher = new TypedFetcher({
  url: 'https://api.example.com',
  endpoints,
});

const result = await fetcher.route('/account/unit/:unit_id', 'get').path({ unit_id: '123' }).exec_json();

if (!result.ok) {
  switch (result.error.type) {
    case 'fetch':
      console.error('network error', result.error.err);
      break;
    case 'http':
      console.error('http error', result.error.response.status, result.error.body);
      break;
    case 'parse':
      console.error('parse error', result.error.err);
      break;
  }
} else {
  console.log(result.http);
}

Defining Endpoints

define_endpoints(...) is a typed identity helper for defining endpoints

import { define_endpoints, endpoint } from '@jeppech/typed-fetcher';

const endpoints = define_endpoints({
  '/users': {
    get: endpoint<{ ok: { id: string }[]; err: { message: string } }>(),
    post: endpoint<
      { ok: { id: string }; err: { message: string } },
      { body: { name: string }; url: never; path: never }
    >(),
  },
});

Making Requests

const result = await fetcher
  .route('/users', 'post')
  .bearer(token)
  .params({ include: 'roles' })
  .json({ name: 'Ada' })
  .exec_json();

Common request helpers:

  • headers(...)
  • basic(username, password)
  • bearer(token)
  • params(...)
  • path(...)
  • body(...)
  • json(...)
  • cache(...)
  • credentials(...)
  • log()
  • exec_json()
  • exec()
  • force_exec()

Result Shape

exec() returns a nested result shape:

  • Outer layer: fetch/network success or failure
  • Inner layer: HTTP success or failure
const result = await fetcher.route('/users', 'get').exec();

if (!result.ok) {
  // fetch failed
  console.error(result.error.type, result.error.message);
} else if (!result.http.ok) {
  // non-2xx HTTP response
  console.error(result.http.status);
} else {
  // 2xx HTTP response
  const body = await result.http.json();
  console.log(body);
}

response.json() and response.text() also return FetchResult<T> so parse failures are surfaced without throwing.

For the common JSON case, exec_json() flattens fetch, HTTP, and parse handling into one result:

const result = await fetcher.route('/users', 'get').exec_json();

if (!result.ok) {
  switch (result.error.type) {
    case 'fetch':
      console.error(result.error.err.type, result.error.err.message);
      break;
    case 'http':
      console.error(result.error.response.status, result.error.body);
      break;
    case 'parse':
      console.error(result.error.err);
      break;
  }
} else {
  console.log(result.http);
}

Fetch failures are currently normalized into the following shape

if (!result.ok) {
  switch (result.error.type) {
    case 'aborted':
      console.log('request was aborted');
      break;
    case 'network':
      console.log('network failure', result.error.message);
      break;
    case 'unknown':
      console.log('unexpected failure', result.error.cause);
      break;
  }
}

Middleware

Middleware wraps request execution and can observe or delay requests before calling next().

fetcher.use(async ({ url, req, next }) => {
  req.headers({ 'x-request-source': 'web' });
  console.log('requesting', url.toString());
  return next();
});

Middleware handlers receive:

  • url: the built request URL
  • req: the current Fetcher instance
  • next: the next handler in the chain

Error Handling

on_error() Use it for logging, metrics, and tracing.

fetcher.on_error(async (err, ctx) => {
  console.warn('request failed', err.type, ctx.url.toString(), ctx.attempt);
});

Use retry() to decide whether a request should be retried.

fetcher.retry(async (err, ctx) => {
  if (err.type === 'fetch' && err.err.type === 'network' && ctx.attempt < 3) {
    return {
      action: 'retry',
      delay_ms: 250,
    };
  }

  if (err.type === 'http' && err.err.status === 401) {
    const token = await refresh_token();

    return {
      action: 'retry',
      patch: {
        bearer: token,
      },
    };
  }

  return { action: 'fail' };
});

retry() accepts an optional second argument:

fetcher.retry(handler, { blocking: true });

When blocking is enabled, the retry handler runs under the fetcher semaphore's blocking lock. If another blocking retry handler is already active, later failed in-flight requests wait for it to finish and then run retry resolution again.

To handle upstream DNS or connection-refused failures, use is_host_unreachable_error(...):

import { is_host_unreachable_error } from '@jeppech/typed-fetcher';

fetcher.retry(async (err) => {
  if (!is_host_unreachable_error(err)) {
    return { action: 'fail' };
  }

  const next_url = await resolver.resolve();

  if (next_url.is_err()) {
    return { action: 'fail' };
  }

  return {
    action: 'retry',
    patch: {
      base_url: next_url.unwrap().toString(),
    },
  };
});

This helper currently checks Node-style network errno codes exposed through fetch such as ENOTFOUND, ECONNREFUSED, EHOSTUNREACH, and ENETUNREACH.

Rate Limiting

Use rate_limiter(...) as middleware when the upstream API exposes rate-limit headers.

import { rate_limiter } from '@jeppech/typed-fetcher';

fetcher.use(
  rate_limiter({
    strategy: 'throttle',
    bucket: (url) => url.pathname,
    burst: 5,
  }),
);

Options:

  • strategy: 'pause' or 'throttle'
  • headers: custom header names for limit, remaining, and reset
  • bucket: rate-limit key. Use (url) => url.pathname to group by route.
  • max_delay_ms: cap any calculated delay
  • burst: number of immediate requests allowed per observed window before throttling starts

burst only affects the 'throttle' strategy. It is capped to the observed server-side limit for the active window.

Concurrency

TypedFetcher also supports a built-in semaphore:

const fetcher = new TypedFetcher({
  url: 'https://api.example.com',
  endpoints,
  semaphore: 4,
});

This limits the number of concurrent requests executed through that fetcher instance.

Exports

Main exports:

  • TypedFetcher
  • Fetcher
  • define_endpoints
  • endpoint
  • rate_limiter
  • BaseError
  • ParseError
  • SerializeError

Type exports include:

  • Endpoint
  • EndpointResponse
  • EndpointSpec
  • HttpResult
  • RequestError
  • RequestContext
  • RetryDecision
  • RetryPatch
  • RateLimitOptions

Development

pnpm lint
pnpm build

There is currently no test runner configured in this repository.