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

abanda

v1.0.9

Published

<div style="display: flex; align-items: center; justify-content: center; margin: 1rem 0;"> <img width="512" height="512" style="border-radius: 10px;" src="https://public.enguidanosweb.com/images/abanda.png"> </div>

Readme

Abanda

Features

  • :helicopter: Global headers
  • :vertical_traffic_light: Route interceptors
  • :vertical_traffic_light: Request interceptors
  • :vertical_traffic_light: Response interceptors
  • :underage: Resource blacklist
  • :link: Base URL with slash normalization
  • :inbox_tray: Accepts string, URL, and Request objects

Install

npm i abanda

API

| Property | Type | Description | | ------------------------- | ----------------------------------- | ------------------------------------------ | | http.base | string | Base URL prepended to relative paths | | http.headers | Headers | Global headers added to every request | | http.blacklist | Set<RequestInfo \| URL> | URLs that will be immediately aborted | | http.intercept.request | Set<HttpRequestInterceptor> | Modify requests before they are sent | | http.intercept.response | Set<HttpResponseInterceptor> | Modify responses after they arrive | | http.intercept.route | Map<RegExp, HttpRouteInterceptor> | Mock or override specific routes | | http.fetch | typeof fetch | Drop-in replacement for globalThis.fetch |

Execution order

1. Base URL resolution
2. Blacklist check → AbortError if matched
3. Global headers merge (request headers take priority)
4. Request interceptors (in insertion order)
5. Route interceptors (first regex match wins, skips real fetch + response interceptors)
6. Real fetch
7. Response interceptors (in insertion order)

Examples

Base URL

import { http } from "abanda";

http.base = "http://localhost:8080";

http.fetch("/api/users").then((response) => response.json());

Slash normalization is handled automatically:

http.base = "http://localhost:8080/";

http.fetch("/api/users").then((response) => response.json());

Absolute URLs bypass the base entirely:

http.base = "http://localhost:8080";

http.fetch("https://other-api.com/data").then((response) => response.json());

Global headers

import { http } from "abanda";

http.headers.set("authorization", "Bearer <TOKEN>");
http.headers.set("x-target", "A");

http.fetch("http://localhost:8080").then((response) => response.json());

Request-specific headers take priority over global headers:

http.headers.set("authorization", "Bearer global-token");

http.fetch("/api/admin", {
  headers: { authorization: "Bearer override-token" },
});

Intercept requests

Request interceptors run before the fetch and can modify headers, method, body, or anything in the RequestInit:

import { http } from "abanda";

http.intercept.request.add(async (url, request) => {
  let headers = request.headers as Headers;
  headers.set("x-request-id", crypto.randomUUID());
  return request;
});

http.fetch("http://localhost:8080/api").then((response) => response.json());

Multiple interceptors run in insertion order — each receives the result of the previous one:

http.intercept.request.add(async (url, request) => {
  let headers = request.headers as Headers;
  headers.set("x-timestamp", Date.now().toString());
  return request;
});

http.intercept.request.add(async (url, request) => {
  console.log(`→ ${request.method} ${url}`);
  return request;
});

Intercept responses

Response interceptors run after the fetch and can inspect, modify, or replace the response:

import { http } from "abanda";

http.intercept.response.add(async (request, response) => {
  if (response.status === 401) {
    const newToken = await refreshToken();
    http.headers.set("authorization", `Bearer ${newToken}`);
    return http.fetch(response.url, request); // Retry with new token
  }
  return response;
});

http.fetch("http://localhost:8080/api").then((response) => response.json());

Retry logic with backoff:

http.intercept.response.add(async (request, response) => {
  if (response.status === 503) {
    let retries = 0;
    let res = response;

    while (retries < 3 && !res.ok) {
      await new Promise((r) => setTimeout(r, 1000 * (retries + 1)));
      res = await fetch(response.url, request); // Use platform fetch to avoid re-intercepting
      retries++;
    }

    return res;
  }

  return response;
});

Inject metadata into every response:

http.intercept.response.add(async (request, response) => {
  const body = await response.json();
  body._fetchedAt = new Date().toISOString();
  return new Response(JSON.stringify(body), {
    status: response.status,
    headers: response.headers,
  });
});

Intercept routes

Route interceptors match against the full resolved URL (including base). The first matching regex wins, and the real fetch is skipped entirely — response interceptors do not run for matched routes.

import { http } from "abanda";

http.intercept.route.set(/\/api\/health$/, async () => {
  return new Response(JSON.stringify({ status: "ok" }), {
    status: 200,
    headers: { "content-type": "application/json" },
  });
});

http.fetch("/api/health").then((response) => response.json());

Use capture groups to extract parameters:

http.intercept.route.set(/\/api\/users\/(\d+)$/, async (url) => {
  const match = (url as string).match(/\/api\/users\/(\d+)$/);
  const userId = match?.[1];

  return new Response(JSON.stringify({ id: userId, name: "Mock User" }), {
    status: 200,
    headers: { "content-type": "application/json" },
  });
});

http.fetch("/api/users/42").then((response) => response.json());

Echo the request body back (useful for testing):

http.intercept.route.set(/\/echo$/, async (url, request) => {
  return new Response(request.body, {
    status: 200,
    headers: { "content-type": "text/plain" },
  });
});

Blacklist

Blacklisted URLs are aborted immediately — no headers are applied, no interceptors run, and no network request is made.

import { http } from "abanda";

http.blacklist.add("/tracking");
http.blacklist.add("/analytics");

http.fetch("/tracking"); // throws DOMException (AbortError)
http.fetch("/api/data"); // works normally

Works with both relative paths and absolute URLs:

http.base = "http://localhost:8080";

http.blacklist.add("/private");
http.blacklist.add("http://localhost:8080/also-private");

http.fetch("/private"); // AbortError
http.fetch("/also-private"); // AbortError

Dynamically manage the blacklist:

// Block a resource temporarily
http.blacklist.add("/maintenance-endpoint");

// Unblock it later
http.blacklist.delete("/maintenance-endpoint");

// Clear all blocks
http.blacklist.clear();

Cleanup

All interceptor collections support standard Set and Map operations:

const logger = async (url: RequestInfo | URL, request: RequestInit) => {
  console.log(`${request.method} ${url}`);
  return request;
};

http.intercept.request.add(logger);
http.intercept.request.delete(logger);

http.intercept.request.clear();
http.intercept.response.clear();
http.intercept.route.clear();

Full example: API client

import { http } from "abanda";

http.base = "https://api.example.com/v1";
http.headers.set("accept", "application/json");

http.intercept.request.add(async (url, request) => {
  const headers = request.headers as Headers;
  const token = getAccessToken();
  headers.set("authorization", `Bearer ${token}`);
  return request;
});

http.intercept.response.add(async (request, response) => {
  if (response.status === 401) {
    await refreshAccessToken();
    return http.fetch(response.url, request);
  }
  return response;
});

if (import.meta.env.DEV) {
  http.intercept.route.set(/\/api\/feature-flags$/, async () => {
    return Response.json({ darkMode: true, beta: false });
  });
}

http.blacklist.add("/telemetry");

const users = await http.fetch("/users").then((r) => r.json());
const me = await http.fetch("/users/me").then((r) => r.json());

License

Abanda is distributed under the MIT license.