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

reqly-js

v0.1.5

Published

A lightweight, typed HTTP client for browsers and Node.js, built on native Fetch with retries, timeouts, and progress tracking.

Readme

reqly-js

Minified Size Minified and Gzipped Size

npm version npm downloads Node.js License Tests

A lightweight, typed HTTP client for browsers and Node.js, built on native Fetch with retries, timeouts, and progress tracking.

  • ✅ Ready-to-use default client
  • ✅ Configured instances with ft.create()
  • ✅ Typed response shortcuts
  • ✅ Browser file downloads with .download()
  • ✅ JSON bodies and search parameters
  • ✅ Timeout and opt-in retries
  • ✅ Typed HTTP, network, and timeout errors
  • ✅ Structured error information with errorInfo()
  • ✅ Upload and download progress with native streams
  • ✅ Request, response, retry, error, and status callbacks
  • ✅ Automatic browser/server base URL selection
  • ✅ Opt-in server header forwarding through a safe allowlist
  • ✅ Native RequestInit options
  • ✅ Zero runtime dependencies

Installation

npm install reqly-js

Node.js 22 or a modern browser with the native Fetch API is required.

Quick start

Import the ready-to-use client:

import ft from "reqly-js";

type Account = {
    email: string;
    id: string;
};

const account = await ft.get("https://api.example.com/accounts/123").json<Account>();

Or create a configured instance:

import ft from "reqly-js";

export const api = ft.create({
    baseUrl: "https://api.example.com",
    headers: {
        accept: "application/json",
    },
    prefix: "v1",
});

const account = await api.get("accounts/123").json<Account>();

The resulting URL is https://api.example.com/v1/accounts/123.

For applications with separate browser and server paths:

const api = ft.create({
    baseUrl: {
        client: "/proxy",
        server: "http://api:4000",
    },
});

The browser uses client; Node.js and other runtimes without window use server.

Methods

All methods accept an optional URL and request options:

ft.get(url, options);
ft.post(url, options);
ft.put(url, options);
ft.patch(url, options);
ft.delete(url, options);
ft.head(url, options);

Calling a method starts one request operation and returns a FetchTask. The task can be awaited as a native Response or consumed through a body shortcut.

const response = await api.get("accounts/123");
const sameResponse = await api.get("accounts/123").response();

Request bodies

Use json to serialize a JSON body and set content-type: application/json when it is not already configured:

const account = await api
    .post("accounts", {
        json: {
            email: "[email protected]",
            name: "Example User",
        },
    })
    .json<Account>();

Use body for any native BodyInit value:

const form = new FormData();
form.set("avatar", file);

const account = await api
    .post("accounts/avatar", {
        body: form,
    })
    .json<Account>();

json and body are mutually exclusive.

Search parameters

Search parameters can be configured on the instance and overridden per request:

const api = ft.create({
    baseUrl: "https://api.example.com",
    searchParams: {
        locale: "en",
    },
});

const accounts = await api
    .get("accounts", {
        searchParams: {
            page: 2,
            role: ["OWNER", "ADMIN"],
        },
    })
    .json<Account[]>();

Supported values are strings, numbers, booleans, null, undefined, and arrays of those values. null and undefined are omitted. Request parameters replace instance parameters with the same name.

An input Request already owns its URL. Passing request-specific searchParams with it throws instead of silently ignoring them.

Response shortcuts

api.get("data").json<MyType>();
api.get("data").text();
api.get("data").blob();
api.get("file").download({ filename: "report.pdf" });
api.get("data").arrayBuffer();
api.get("data").formData();
api.get("data").response();

json<T>() defaults to unknown. The generic type provides compile-time typing only; it does not validate the response at runtime. Empty or invalid JSON rejects with the native parsing error.

File downloads

Use .download() to save a response directly in the browser:

await api
    .post("invoices/pdf", {
        json: { invoice_id: "invoice-id" },
        onDownloadProgress: ({ percent, transferred, total }) => {
            console.log(percent, transferred, total);
        },
    })
    .download({ filename: "invoice.pdf" });

The explicit filename has priority. When omitted, the name is read from the standard Content-Disposition response header and falls back to download. Path segments are removed from filenames before the browser receives them.

.download() is browser-only and rejects with a TypeError in server runtimes. Use .blob(), .arrayBuffer(), or .response() when the response must be processed on the server.

Configuration

Instance options

| Property | Type | Default | Description | | ----------------- | ------------------------------------------- | ------- | ----------------------------------------------------- | | baseUrl | string \| URL \| RuntimeBaseUrl | — | Static URL or automatic client/server URLs. | | prefix | string | — | Path inserted between baseUrl and the request path. | | searchParams | SearchParams | — | Parameters included in every request. | | headers | HeadersInit | — | Headers included in every request. | | forwardHeaders | boolean \| { extra: string[] } | false | Forwards allowlisted incoming headers on the server. | | getHeaders | () => HeadersInit \| Promise<HeadersInit> | — | Provides the current incoming server headers. | | timeout | number \| false | false | Request timeout in milliseconds. | | retry | number \| RetryConfig \| false | false | Enables retries. A number is the retry limit. | | throwHttpErrors | boolean | true | Throws HTTPError for non-2xx responses. | | beforeRequest | BeforeRequest | — | Runs before every attempt. | | afterResponse | AfterResponse | — | Runs after every received response. | | onRetry | OnRetry | — | Runs before a retry delay. | | onError | OnError | — | Observes or replaces the final error. | | onStatus | StatusHandlers | — | Runs an action for the final response status. |

All other native RequestInit properties, such as cache, credentials, mode, and redirect, are supported.

Request options

Request options support the same reliability, lifecycle, and native options, plus:

| Property | Type | Description | | -------------------- | -------------------- | ----------------------------------------------------------------- | | json | unknown | Serializes a JSON request body. | | body | BodyInit \| null | Sends a native request body. | | searchParams | SearchParams | Adds or replaces search parameters. | | signal | AbortSignal | Cancels the request without being replaced by the timeout signal. | | onUploadProgress | (progress) => void | Reports native upload stream progress. | | onDownloadProgress | (progress) => void | Reports native download stream progress. |

Request-specific lifecycle callbacks replace the matching instance callback. They are not silently chained.

Runtime URLs and header forwarding

Runtime selection and header filtering are framework-independent. Only the function that obtains the current incoming request headers belongs to the application:

const api = ft.create({
    baseUrl: {
        client: "/proxy",
        server: process.env.API_URL!,
    },

    getHeaders: async () => {
        const { headers } = await import("next/headers");
        return headers();
    },

    forwardHeaders: true,
});

forwardHeaders: true enables the built-in allowlist:

accept-language
cf-connecting-ip
origin
referer
sec-ch-ua
sec-ch-ua-mobile
sec-ch-ua-platform
sec-fetch-dest
sec-fetch-mode
sec-fetch-site
sec-fetch-user
true-client-ip
user-agent
x-forwarded-for
x-forwarded-host
x-forwarded-port
x-forwarded-proto
x-real-ip

Add application-specific headers without replacing the defaults:

const api = ft.create({
    baseUrl: {
        client: "/proxy",
        server: process.env.API_URL!,
    },
    getHeaders,
    forwardHeaders: {
        extra: ["x-tenant-id"],
    },
});

The property is disabled when omitted or set to false. On the server, enabling it without getHeaders throws a configuration error. In the browser, getHeaders is not called because the browser controls its own outgoing request headers.

cookie and authorization are intentionally excluded from the default allowlist. Add them explicitly only when the destination is trusted:

forwardHeaders: {
    extra: ["cookie", "authorization"],
}

Forwarded headers have the lowest priority. Instance headers, headers from an input Request, and request-specific headers override them in that order. getHeaders is called once per operation, not once per retry.

The application and its reverse proxy remain responsible for ensuring IP and forwarding headers are trustworthy before they reach the fetcher.

Retries

Retries are disabled by default. Enable them with a number:

const api = ft.create({
    retry: 2,
});

Or configure them explicitly:

const api = ft.create({
    retry: {
        baseDelay: 300,
        jitter: true,
        limit: 2,
        maxDelay: 30_000,
        methods: ["GET", "HEAD"],
        statusCodes: [408, 429, 500, 502, 503, 504],
    },
});

Only GET and HEAD are retried by default. Add mutation methods explicitly only when the endpoint is idempotent. Request streams are never replayed or buffered silently. A Retry-After value within maxDelay is followed exactly without jitter. Responses requesting a longer delay are not retried.

Timeout and cancellation

const account = await api
    .get("accounts/123", {
        timeout: 10_000,
    })
    .json<Account>();

The timeout covers all attempts and retry delays until the final response headers are received. A timeout throws TimeoutError. A user-provided AbortSignal remains independent and preserves its own abort reason.

Lifecycle

const api = ft.create({
    beforeRequest: ({ attempt, isServer, request }) => {
        request.headers.set("x-attempt", String(attempt));
        request.headers.set("x-runtime", isServer ? "server" : "client");
    },

    afterResponse: ({ response, attempt, request, isServer }) => {
        console.log(response.status, attempt, request, isServer);
    },

    onRetry: ({ attempt, delay, error, isServer }) => {
        console.log({ attempt, delay, error, isServer });
    },

    onError: ({ error, attempt, request, isServer }) => {
        console.log({ error, attempt, request, isServer });
        return new Error("API request failed", { cause: error });
    },
});

The order is:

beforeRequest
  -> fetch
  -> afterResponse
  -> onRetry (when another attempt will run)
  -> onStatus (final response only)
  -> onError (final fetcher error only)

afterResponse may return a replacement Response. onError may return a replacement Error. Every lifecycle callback receives isServer, calculated once when the request operation starts.

Status actions

onStatus runs after retries and before an HTTPError is created:

const api = ft.create({
    onStatus: {
        401: ({ isServer, request }) => {
            if (!isServer) {
                window.location.replace("/login");
            }

            console.log("Unauthorized", request.url);
        },
        503: () => {
            throw new Error("Maintenance mode");
        },
    },
});

Errors thrown by a status action propagate unchanged and do not pass through onError. This allows the application to use its own routing or control-flow mechanism.

Progress

await api
    .post("upload", {
        body: file,
        onUploadProgress: ({ percent, transferred, total }) => {
            console.log({ percent, transferred, total });
        },
    })
    .json();

await api
    .get("download", {
        onDownloadProgress: ({ percent, transferred, total }) => {
            console.log({ percent, transferred, total });
        },
    })
    .download({ filename: "download.bin" });

total and percent are null when the runtime or server does not provide a known size. Upload progress depends on native request stream support. The package does not switch to XMLHttpRequest or another transport.

Errors

import { errorInfo, FetchError, HTTPError, NetworkError, TimeoutError } from "reqly-js";
  • HTTPError exposes request and response.
  • NetworkError exposes request and the native error through cause.
  • TimeoutError exposes request and timeout.
  • FetchError is the shared base class.

Use errorInfo() to obtain a consistent result without consuming the original response:

try {
    await api.get("accounts");
} catch (err) {
    const { code, message, status } = await errorInfo(err);
    console.log({ code, message, status });
}

It reads error, message, and code from JSON error responses. HTTP errors use the response status; errors without an HTTP response use status 0. Unknown errors return Request failed.

Current scope

This version contains only the framework-independent native Fetch client. It can select browser/server URLs and filter incoming headers, but it never imports a framework or discovers a framework request context by itself. The consuming application provides that context through getHeaders. Authentication, session management, and application caching remain outside the package.

License

MIT