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

api2client

v2.0.2

Published

Hey API client that sends generated OpenAPI SDK requests with grab instead of axios or fetch, adding caching, retries, rate limiting, deduplication and mocks.

Readme

api2client

Generate a fully typed SDK from an OpenAPI spec with Hey API, then let grab send the requests instead of axios or fetch.

The generated code is unchanged — the same getPet(), createPet() functions, the same types. Only the transport underneath is swapped, so every endpoint in the SDK gains caching, retries, rate limiting, request deduplication, a mock server and the shared request log.

import { getPet } from "./client";

const { data, error, response } = await getPet({
  path: { petId: "42" },
  cache: true,        // grab options work per request
  retryAttempts: 2,
});

Install

npm i api2client
npm i -D @hey-api/openapi-ts

Generate

npx api2client https://petstore3.swagger.io/api/v3/openapi.json ./src/client

That runs @hey-api/openapi-ts and rewires the output to this client. Options:

| Option | Purpose | | ------------------------ | ---------------------------------------------------------- | | -i, --input <path\|url> | OpenAPI spec to generate from | | -o, --output <dir> | Where to write the SDK — default ./src/client | | -c, --client <name> | Hey API client to generate against — default @hey-api/client-fetch | | --rewire-only | Skip generation, rewire an SDK you already generated | | --no-rewire | Generate without swapping in the grab client |

Anything else is forwarded to the openapi-ts CLI. The same thing from Node:

import { generateFromOpenAPI } from "api2client";

await generateFromOpenAPI({ input: "./openapi.yaml", output: "./src/client" });

Already generating with your own openapi-ts.config.ts? Keep it, and rewire afterwards:

// package.json
"scripts": {
  "codegen": "openapi-ts && api2client --rewire-only ./src/client"
}

Configure

The generated client.gen.ts exports a client you can configure once for the whole SDK:

import { client } from "./client/client.gen";

client.setConfig({
  baseUrl: "https://api.example.com",
  auth: () => localStorage.getItem("token"),

  // grab options, applied to every request in the SDK
  cache: true,
  cacheForTime: 60,
  retryAttempts: 2,
  rateLimit: 1,
  timeout: 15,
});

Or build a client yourself:

import { createClient, createConfig } from "api2client";

const client = createClient(createConfig({ baseUrl: "https://api.example.com" }));

Defaults

createConfig() starts every client with these, so a generated SDK gets grab's behavior without being configured:

import { defaultGrabOptions } from "api2client";

// { cache: false, cacheForTime: 60, retryAttempts: 2, timeout: 30 }

cache is configured but off: an SDK that served a GET from a minute-old cache would hand back pre-write data after a POST to the same resource, and that is not something a caller can opt out of after the fact. Turn it on with cache: true — the window is already set.

cache and retryAttempts reach GET, HEAD and OPTIONS only. Setting either client-wide leaves writes alone, since serving a POST from cache or replaying a DELETE that failed changes what the API was asked to do; a single write can still ask for them per request.

grab options

Every option below is accepted client-wide in createConfig()/setConfig() and per request. See grab options for the full reference.

| Option | Effect | | -------------------------------------- | -------------------------------------------------- | | cache, cacheForTime | Serve repeated requests from grab's frontend cache | | retryAttempts | Retry failed requests | | rateLimit | Minimum seconds between requests to the same path | | timeout | Seconds before the request is aborted | | cancelOngoingIfNew, cancelNewIfOngoing | Deduplicate concurrent requests to the same path | | debug, logger | Log requests and responses | | unzip, parseDOM, unescapeHTML | Opt back into grab's ZIP/HTML post-processing | | grab | Use a custom grab instance, e.g. grab.instance({}) | | devtools | default=on localhost Attach the Ctrl+Alt+I request inspector |

Inspecting requests — Ctrl+Alt+I

In development, creating a client attaches grab's request inspector: Ctrl+Alt+I opens a modal listing every request the SDK made, with its parsed response.

grab keeps that log on the global window.grab rather than on the instance the SDK holds, so the client publishes its grab there when nothing else has — without that the shortcut opens onto a log the SDK never wrote to. An app that imports grab itself keeps its own global, and its log, untouched. It is bound once per page however many clients are created.

It stays off in production. A public origin is someone's production site, and every request the SDK made is not something to hand its visitors a keystroke away, so devtools left unset follows grab's own gate: on for a loopback host, off everywhere else. It also does nothing outside a browser.

Both overrides are explicit:

createClient(createConfig({ baseUrl, devtools: true }));   // deployed build
createClient(createConfig({ baseUrl, devtools: false }));  // off, even locally

Mock any endpoint

grab.mock keys are request paths (with or without a leading slash), so an SDK endpoint can be stubbed without touching the network:

import { grab } from "grab-api.js";

grab.mock["/pets/42"] = { response: { id: "42", name: "Rex" } };

const { data } = await getPet({ path: { petId: "42" } }); // { id: "42", name: "Rex" }

What you get back

The standard Hey API result — { data, error, request, response }, or just data with responseStyle: "data", or a thrown error with throwOnError: true:

const { data, error, response } = await getPet({ path: { petId: "99" } });

if (error) console.log(response.status, error); // 404 { message: "Pet not found" }

buildUrl(), interceptors.request/response/error, security/auth, bodySerializer, querySerializer, parseAs, responseValidator and responseTransformer all behave as they do in the official clients.

Server-sent events

An operation whose response is text/event-stream generates an SDK function that calls client.sse.<method>() instead of client.<method>(), and returns { stream } — an async generator of parsed event data:

import { streamJobEvents } from "./client";

const { stream } = await streamJobEvents({ path: { jobId: "42" } });

for await (const event of stream) console.log(event); // parsed `data:` payload

SSE is a long-lived connection, not a single request/response, so it connects with fetch directly instead of going through grab — grab's cache/retry/rate-limit model is built around a request that completes, not one that stays open and reconnects on its own. Pass these alongside the usual request options:

| Option | Effect | | ------------------------- | ------------------------------------------------------------------- | | onSseEvent | Called for every event, with its data, event, id and retry | | onSseError | Called when a connection attempt fails, before it retries | | sseDefaultRetryDelay | default=3000 Delay before the first retry, in ms | | sseMaxRetryAttempts | Give up after this many retry attempts | | sseMaxRetryDelay | default=30000 Cap on the exponential backoff delay, in ms | | fetch | Fetch implementation to use — default=globalThis.fetch |

Reconnects honor the server's retry: field and send Last-Event-ID from the last event's id:, matching browser EventSource semantics. A stream that ends normally (the server closes the connection) does not reconnect — only a network or parse error does.

Differences from @hey-api/client-fetch

  • Transport failures are returned, not thrown. A timeout or connection failure comes back as { error } (or throws when throwOnError is set), matching grab's "errors are data" behavior. HTTP error statuses behave the same as in the official clients.
  • Bodies are parsed by grab. parseAs: "stream" hands you the raw stream, and an explicit parseAs still decides the empty-response shape, but otherwise grab's content-type detection reads the body.
  • grab sets JSON Content-Type/Accept defaults on requests that do not specify their own, including body-less ones.
  • Response interceptors run before parsing, on a response whose body grab has already read.
  • sse endpoints bypass grab and connect with fetch directly — see Server-sent events.

Requirements

Requests are sent with grab-api.js — the grab() request function on its own, without grab-url's loading icons or the linkedom/archiver-web HTML and archive extractors an OpenAPI response never needs. Its default entry is the slim build, which keeps the install and bundle small.

grab-api.js and grab-url are separate packages, so each has its own mock and log. To stub or inspect SDK requests, import grab from grab-api.js, as above — a stub registered on grab-url's grab is invisible to the SDK.

grab itself is never bundled here. dist/index.es.js is about 20 kB, and a generated SDK plus this transport layer stays well under 100 kB; the heavy /full build is unreachable from it.

Needs a grab-api.js (or older grab-url) whose slim entry advertises the onRawResponse hook — check grab.supports?.onRawResponse — which is what reports the response status, headers and parsed error payloads. The slim executor has called the hook since 1.6.23, but the slim entry did not set the flag until the release this client ships with, so an older slim grab silently fell back.

The client detects support and never sends options an older grab would turn into query parameters, so it still runs on one that lacks the flag — but a failed request then comes back as grab's error message ("HTTP error: 404 Not Found") with no response at all, so result.response.status throws, and retryAttempts/cacheForTime are not applied. Anything that branches on the status needs the flag.

What's in this package

| Path | Purpose | | -------------------------------------------------------- | ---------------------------------------------------- | | src/client.ts | createClient() — the Hey API interface over grab | | src/types.ts | The client contract generated SDKs type-check against | | src/utils.ts | Config merging, URL building, auth, interceptors, defaultGrabOptions | | src/core/ | OpenAPI path/query/body serializers, SSE streaming | | src/generate.ts | Codegen and rewiring of generated output | | src/cli.ts | The api2client command |

Serialization and SSE streaming in src/core/ are ported from Hey API's client core (MIT) so generated SDKs produce identical URLs, bodies and event streams on any client.

Agent Skill

Teach your coding agent (Claude Code, Cursor, Gemini, Codex, Antigravity, etc.) how to use this package with the use-api2client skill.

One command:

npx skills add OpenSourceAGI/GRAB-URL@use-api2client

Or manually, copy the skill into your agent's skills folder:

mkdir -p ~/.claude/skills/use-api2client
curl -fsSL https://raw.githubusercontent.com/OpenSourceAGI/GRAB-URL/master/skills/use-api2client/SKILL.md \
  -o ~/.claude/skills/use-api2client/SKILL.md

Use .claude/skills/use-api2client/SKILL.md inside a project instead of ~/.claude/... to scope it to that repo.