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

thusdev-fetch

v1.0.4

Published

A lightweight, TypeScript-first HTTP client with security controls, safe retries, interceptors, plugins, caching, streaming responses, observability, and request timing.

Readme

ThusDev Fetch

CI npm version npm downloads License

A lightweight, TypeScript-first HTTP client built on native fetch for modern applications.

ThusDev Fetch provides safe retries, timeouts, interceptors, plugins, structured errors, secure debug logging, caching, request metrics, and request timing without replacing the simplicity of fetch.

Concept

ThusDev Fetch is more than an HTTP client.

It is a developer experience layer for working with APIs.

Built on native fetch, it keeps the simplicity of standard web APIs while adding the controls and abstractions needed for modern applications: retries, timeouts, caching, interceptors, plugins, structured errors, security controls, metrics, and request lifecycle hooks.

The goal is simple: make API calls easier to write, safer to operate, and easier to standardize across projects.

Why This Exists

Most HTTP clients focus on providing a broad set of features.

ThusDev Fetch focuses on the developer experience around API calls.

It reduces repetitive request logic and provides a consistent way to handle common concerns such as errors, retries, timeouts, authentication, caching, logging, and observability.

Instead of replacing fetch, ThusDev Fetch builds on it to provide a lightweight and extensible layer that teams can adapt to their own applications.

Requirements

  • Node.js 18 or newer
  • A runtime with fetch, Headers, Request, Response, AbortController, FormData, and Blob

Installation

npm install thusdev-fetch

Quick start

import { createClient } from "thusdev-fetch";

const api = createClient({
  baseURL: "https://api.example.com",
  timeout: 5000,
  retry: 2
});

type User = {
  id: number;
  name: string;
};

const user = await api.get<User>("/users/1");
console.log(user.name);

Features

  • Native fetch foundation
  • TypeScript generics
  • All common HTTP methods
  • JSON, text, FormData, Blob, ArrayBuffer, and URLSearchParams bodies
  • Base URL support
  • Standard fetch options
  • Timeout and external abort signals
  • Exponential backoff with optional jitter
  • Retry-After support
  • Safe retry defaults for idempotent methods
  • Explicit unsafe-method retry opt-in
  • Request and response interceptors
  • Async plugins
  • Memory caching for GET requests
  • ETag and Last-Modified revalidation
  • Optional stale-while-revalidate behavior
  • Structured errors
  • Secure debug logging
  • Request lifecycle hooks
  • Request metrics
  • Authentication and logging plugins
  • SSRF-aware URL validation and host allowlisting
  • Safe redirect default (error)
  • Request and response size limits
  • Bounded LRU-style memory cache
  • Total request deadline across retries
  • Guarded redirects with destination validation and configurable limits
  • Typed response modes for JSON, text, ArrayBuffer, Blob, and streams

Client factory

Use createClient when you want a concise API:

const api = createClient({
  baseURL: "https://api.example.com",
  security: {
    allowedHosts: ["api.example.com"],
    maxRequestSize: 1024 * 1024,
    maxResponseSize: 5 * 1024 * 1024
  }
});

new ThusFetch() remains fully supported.

HTTP methods

await api.get<User>("/users/1");
await api.post<User>("/users", { name: "Ada" });
await api.put<User>("/users/1", { name: "Ada Lovelace" });
await api.patch<User>("/users/1", { name: "Ada" });
await api.delete("/users/1");
await api.head("/users/1");
await api.options("/users/1");

Request bodies

Plain objects are serialized as JSON. Existing body types are passed through without modification.

await api.post("/users", { name: "Ada" });

const form = new FormData();
form.set("name", "Ada");
await api.post("/users", form);

await api.post("/query", new URLSearchParams({ q: "thusdev" }));

Retries

Retries are disabled by default.

await api.get("/users", {
  retry: {
    attempts: 3,
    delay: 100,
    maxDelay: 2000,
    factor: 2,
    jitter: true,
    retryAfter: true
  }
});

Transient HTTP statuses include 408, 425, 429, 500, 502, 503, and 504.

By default, retries are allowed for idempotent methods: GET, HEAD, OPTIONS, PUT, and DELETE. POST and PATCH are not retried unless explicitly enabled.

await api.post("/payments", payload, {
  retry: {
    attempts: 2,
    retryUnsafeMethods: true
  }
});

Custom retry logic receives the error and retry context:

await api.get("/users", {
  retry: {
    attempts: 2,
    retryOn: (error, context) => {
      console.log(context.attempt, error);
      return true;
    }
  }
});

Security configuration

Security controls are available through security and are designed to reduce common client-side SSRF, resource-exhaustion, credential-leakage, and unsafe-redirect risks.

const api = createClient({
  baseURL: "https://api.example.com",
  security: {
    allowedHosts: ["api.example.com"],
    maxRequestSize: 1024 * 1024,
    maxResponseSize: 5 * 1024 * 1024
  },
  redirect: "error"
});

HTTP and HTTPS are allowed by default. Embedded URL credentials and loopback/private IPv4 and IPv6 literal destinations are rejected by default. allowedHosts is recommended when URLs can be influenced by untrusted input. Private-network access can be explicitly enabled with allowPrivateNetwork for trusted internal environments.

Response caching does not store responses marked private or no-store, and the in-memory cache is bounded. Avoid caching personalized or sensitive responses unless the cache key is deliberately isolated.

See Security for the threat model and deployment guidance.

Timeout and cancellation

const controller = new AbortController();

await api.get("/users", {
  timeout: 5000,
  deadline: 15000,
  signal: controller.signal
});

controller.abort();

Timeouts produce ThusError with code TIMEOUT. User cancellation produces ABORTED. A configured deadline limits the entire operation across retries and produces DEADLINE_EXCEEDED.

Interceptors

const requestId = api.interceptors.useRequest((config) => {
  const headers = new Headers(config.headers);
  headers.set("x-client", "thusdev");
  return { ...config, headers };
});

const responseId = api.interceptors.useResponse((response) => response);

api.interceptors.ejectRequest(requestId);
api.interceptors.ejectResponse(responseId);

Interceptors can be asynchronous and execute in registration order.

Plugins

api.plugins.use({
  beforeRequest: async (config) => config,
  afterResponse: async (response) => response
});

Plugins can be removed:

const plugin = {
  beforeRequest: async (config) => config
};

api.plugins.use(plugin);
api.plugins.remove(plugin);

Authentication plugin

import { createAuthPlugin } from "thusdev-fetch";

api.plugins.use(createAuthPlugin(async () => getAccessToken()));

The plugin only adds Authorization when a request does not already provide one.

Logging plugin

import { createLoggingPlugin } from "thusdev-fetch";

api.plugins.use(createLoggingPlugin());

Sensitive headers are redacted by the built-in logger.

Caching

Caching is opt-in and applies to GET requests.

const user = await api.get<User>("/users/1", {
  cacheOptions: {
    ttl: 30000
  }
});

ETag and Last-Modified values are used for conditional revalidation after an entry expires.

For authenticated requests, caching is disabled by default unless an explicit cache key is supplied:

await api.get("/me", {
  headers: {
    Authorization: `Bearer ${token}`
  },
  cacheOptions: {
    key: `me:${userId}`,
    ttl: 30000
  }
});

Stale-while-revalidate can return an expired entry immediately while refreshing it in the background:

await api.get("/config", {
  cacheOptions: {
    ttl: 30000,
    staleWhileRevalidate: true
  }
});

The cache can be managed directly:

api.clearCache();
api.invalidateCache("GET:https://api.example.com/users");

Hooks and metrics

const api = createClient({
  onRequestStart: ({ method, url }) => {
    console.log("start", method, url);
  },
  onRequestEnd: ({ status, duration, error }) => {
    console.log("end", status, duration, error);
  },
  onRetry: ({ attempt, method, url }) => {
    console.log("retry", attempt, method, url);
  }
});

const metrics = api.getMetrics();
api.resetMetrics();

Metrics expose total requests, successes, failures, retries, and cumulative duration.

Errors

import { ThusError } from "thusdev-fetch";

try {
  await api.get("/users/1");
} catch (error) {
  if (error instanceof ThusError) {
    console.log(error.code);
    console.log(error.status);
    console.log(error.method);
    console.log(error.url);
    console.log(error.response);
    console.log(error.retryAfter);
  }
}

Available codes:

  • NETWORK_ERROR
  • TIMEOUT
  • HTTP_ERROR
  • ABORTED
  • UNKNOWN_ERROR
  • SECURITY_ERROR
  • REQUEST_TOO_LARGE
  • RESPONSE_TOO_LARGE
  • DEADLINE_EXCEEDED

Debugging

import { setDebug } from "thusdev-fetch";

setDebug(true);

Debug logging includes method, sanitized URL, status, and duration. Authorization, cookies, API keys, proxy authorization, and common token headers are redacted. Common sensitive query parameters are also redacted. Keep debug logging disabled in production unless explicitly required.

Response types

Use responseType when the response is not JSON or when streaming is required:

const file = await api.get<ArrayBuffer>("/file", { responseType: "arrayBuffer" });
const stream = await api.get<ReadableStream<Uint8Array>>("/large-file", { responseType: "stream" });

Supported values are auto, json, text, arrayBuffer, blob, and stream.

TypeScript

Every HTTP method accepts a response generic:

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

const product = await api.get<Product>("/products/1");

Security and quality

The project includes CI, dependency auditing, CodeQL analysis, dependency review, Dependabot configuration, deterministic tests, and security-focused tests. The implementation follows OWASP-aligned practices relevant to an HTTP client; it does not claim universal OWASP compliance because application-level controls depend on the deployment environment.

Read the detailed guides:

Development

npm install
npm run typecheck
npm test
npm run build
npm run pack:check
npm run benchmark

The test suite is deterministic and uses mocked fetch calls. It does not require network access.

Benchmark iterations can be changed:

BENCHMARK_ITERATIONS=10000 npm run benchmark

The benchmark reports both thusdev-fetch and native fetch under the same local mocked transport. These numbers measure this repository's overhead in one controlled scenario and are not a universal performance ranking.

Release

Create a version tag after validation:

npm run typecheck
npm test
npm run build
npm pack --dry-run
git tag v1.0.3
git push origin v1.0.3

The GitHub release workflow validates the package and publishes it to npm with provenance. No npm token is stored in the repository; publishing is performed through npm trusted publishing/OIDC.

Project structure

src/
├── core/
├── features/
├── plugins/
└── utils/

benchmark/
test/
.github/

Documentation

Development & Testing

  • Test Commands — Complete reference of commands for testing, validation, security checks, benchmarking, and release preparation.
  • Pre-Deployment Checklist — Complete checklist to run before releasing or deploying a new version.

Security

See SECURITY.md for the security policy and vulnerability reporting process.

Contributing

See CONTRIBUTING.md for contribution guidelines.

License

MIT © 2026 ThusDev

Built in Benin. Built for developers everywhere.