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

@vocab-bloom-hub/client

v1.3.0

Published

Typed Node.js / browser client for the Vocab Bloom Hub public dictionary API (/api/v1).

Readme

@vocab-bloom-hub/client

Typed client for the public read-only API of a Vocab Bloom Hub instance — an English dictionary with IPA, CEFR levels, sense-level definitions, examples, translations (Russian, Spanish, French, German, Portuguese, Chinese, Arabic) and inflected forms, served under /api/v1.

Documentation, the API reference and a playground: vocab-bloom-hub.com.

  • One method per endpoint, typed from the server's OpenAPI document — the types cannot drift from the API.
  • Node.js ≥ 20 and browsers; fetch only, no dependencies; ESM and CommonJS.
  • Errors are thrown as typed exceptions; AbortSignal on every call; optional ETag cache for repeated reads; opt-in retry on 429 / 5xx honouring Retry-After; a versioned User-Agent.

Install

npm install @vocab-bloom-hub/client

Stable releases publish under latest. A prerelease publishes under its channel dist-tag (alpha, beta) and never takes latest, so it has to be named: npm install @vocab-bloom-hub/client@beta.

Quick start

import { VocabBloomClient, NotFoundError } from '@vocab-bloom-hub/client';

const client = new VocabBloomClient({ baseUrl: 'https://dict.example.com' });

// search: relevance tiers, typo tolerance
const { data, meta } = await client.search({ search: 'definately' });
console.log(meta.fuzzy, data[0].word); // true "definitely"

// a headword with every part of speech, forms, meanings and translations
try {
  const run = await client.word('run');
  console.log(run.data[0].meanings[0].definition);
} catch (error) {
  if (error instanceof NotFoundError) console.log('no such word');
  else throw error;
}

// walk the whole dictionary, page after page
for await (const word of client.iterateWords({ word_level: ['A1', 'A2'], with_meanings: true })) {
  console.log(word.word, word.meanings.length);
}

Runnable cursor pagination example

examples/cursor-pagination.mjs walks the verbs of an instance with client.words(), two entries per page. It prints the dataset's metadata and terms first, then each page's entries with their origins, contributions and licenses, and labels entries changed on the instance (see License).

With Node.js ≥ 20, save the linked file as cursor-pagination.mjs in an empty directory, then run:

npm install @vocab-bloom-hub/client
node cursor-pagination.mjs https://dict.example.com

Replace the URL with your instance's base URL; /api/v1 is appended by the client. The example imports the package's public ESM export and needs no other dependency. From a repository checkout, use the pinned Yarn and build the SDK first (Node.js ≥ 22.13):

yarn install --immutable
yarn workspace @vocab-bloom-hub/client build
node packages/npm-sdk/examples/cursor-pagination.mjs http://localhost:3010

For an isolated local walkthrough, run yarn workspace server fixture:public-api in another terminal (Node.js ≥ 24.9). It uses an in-memory SQLite database and prints LISTENING <url>. Use that URL in the command above: its three verbs produce two pages and Done: 3 entries across 2 page(s). Stop the fixture with Ctrl+C when finished.

The first request omits cursor. Every subsequent request keeps limit and part_of_speech unchanged and passes meta.next_cursor back exactly as received; the cursor is opaque. An absent cursor ends the walk, even on a full final page. An empty first page prints No entries match the filter. and completes without an extra request. SDK failures go to stderr and set a non-zero exit code; the example does not retry automatically.

When you only need entries, replace the manual loop with the existing convenience API:

for await (const word of client.iterateWords({ limit: 2, part_of_speech: ['verb'] })) {
  console.log(`${word.word}${word.modified ? ' (changed on this instance)' : ''}`);
  console.log(JSON.stringify(word, null, 2));
}

iterateWords() fetches pages lazily and follows their cursors for you. Keep the metadata display and error handling around it. Running both loops reads the same entries twice.

API

| Method | Endpoint | Answer | | ----------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | search(request) | GET /search | SearchResponse | | searchDetailed(request) | GET /search/detailed | DetailedSearchResponse | | word(headword) | GET /words/{word} | HeadwordResponse | | wordsBatch(words) | POST /words/batch | WordsBatchResponse — up to 50 headwords, one rate-limit unit; misses under meta.not_found | | wordById(id) | GET /words/id/{id} | WordResponse | | meanings(headword) | GET /words/{word}/meanings | MeaningsResponse | | translations(headword, query?) | GET /words/{word}/translations | TranslationsResponse | | forms(headword) | GET /words/{word}/forms | FormsResponse | | synonyms(headword) | GET /words/{word}/synonyms | LinksResponse — the linked headwords per meaning | | antonyms(headword) | GET /words/{word}/antonyms | LinksResponse | | history(headword) | GET /words/{word}/history | HistoryResponse — what was changed on the instance, with the values before and after | | wordDatasets(headword) | GET /words/{word}/datasets | WordDatasetsResponse — the headword in every dataset of the instance, a group per dataset with its terms | | datasetHistory(headword, dataset) | GET /words/{word}/datasets/{dataset}/history | HistoryResponse — the edits of the headword in one dataset | | words(query?) | GET /words | WordsResponse (one page) | | iterateWords(query?) | GET /words, following the cursor | AsyncGenerator<Word> | | iterateSearchDetailed(request) | GET /search/detailed, page after page | AsyncGenerator<Word> — stops at the server's page cap (DETAILED_SEARCH_MAX_PAGE, 20) | | random(filters?) | GET /random | WordResponse | | meta() | GET /meta | MetaResponse | | openapi() | GET /openapi.json | the OpenAPI 3 document | | suggest(request) | POST /suggestions | SuggestionCreatedResponse — files a reader report (or an edit proposal) into the instance's moderation queue |

For a report from a dataset tab, pass its dataset name: client.suggest({ headword: "run", dataset: "reader_words", message: "Please check this definition." }). Omitting it uses the active dataset. Unknown, closed or deleted targets raise NotFoundError; the client-wide rate limit is shared across targets.

Every method resolves to the { data, meta } envelope the API answers with and takes an optional last argument { signal, headers, timeoutMs }. The request and response types (Word, Meaning, SearchRequest, ListWordsQuery, …) are exported — the request types of the GET reads are their query strings, so a SearchRequest is { search, type?, limit? } — and so are the raw generated paths / components / operations for anything not aliased. The contract itself — tiers, filters, cursor pagination, caching — is documented in the server's docs/api.md.

Options

new VocabBloomClient({
  baseUrl: 'https://dict.example.com', // origin of the instance; /api/v1 is appended
  headers: { 'X-App': 'my-app' }, // sent with every request
  cache: true, // ETag revalidation (see below); or your own ResponseCache
  fetch: myFetch, // a custom fetch (instrumentation, polyfills, tests)
  timeoutMs: 10_000, // fail a hung request with NetworkError; null disables (the default is 10 s)
  retry: { attempts: 3, backoffMs: 500, maxDelayMs: 60_000 }, // opt-in: retry the GET reads on 429 / 5xx (see below)
});

timeoutMs can also be passed per call and combines with your signal — whichever fires first aborts the request.

Every request carries User-Agent: vocab-bloom-hub-npm/<version> (USER_AGENT, built from SDK_VERSION) so an operator can tell SDK traffic apart in the log; pass your own User-Agent in headers to replace it. Browsers own the header and ignore the value.

Retry

Off by default — the client documents exact request counts against the rate limit, so the loop is opt-in. With retry: {} (or explicit attempts / backoffMs) a GET answered 429 or 5xx is sent again: after Retry-After when the server sent it, otherwise after backoffMs, then twice that, and so on, up to attempts tries in total (the first one included; 3 and 500 ms by default); no single wait exceeds maxDelayMs (60 s by default), whatever Retry-After says. POST requests (the batch lookup, a suggestion), 4xx answers and network errors are never retried; an abort of your signal while waiting ends the wait with NetworkError.

Errors

Failed requests throw:

| Class | When | Fields | | ----------------- | ----------------------------------------------- | ---------------------------------------------- | | NotFoundError | 404 | status, code (word_doesnt_found), body | | RateLimitError | 429 — the public rate limit | retryAfter (seconds, from Retry-After) | | NetworkError | no answer: DNS, connection, TLS, abort, timeout | status: 0, code: 'network_error', cause | | VocabBloomError | everything else | status, code, body |

Without the retry option the client never retries on its own: a RateLimitError carries retryAfter (seconds) and backoff is the caller's decision.

code is the machine-readable error of the API (invalid_cursor, too_many_requests, …), or http_error when the answer was not JSON (a proxy page, for instance).

ETag cache

With cache: true every GET answer is kept in memory per URL together with its ETag; the next read of the same URL sends If-None-Match and, on 304 Not Modified, returns the kept body — the round trip stays, the payload does not. MemoryCache holds 500 entries (least recently used out); pass an object with get(url) / set(url, entry) for a store of your own. Off by default.

Development

yarn workspace @vocab-bloom-hub/client generate        # types from apps/server/openapi/public-v1.json
yarn workspace @vocab-bloom-hub/client generate:check  # fail when the generated types are stale (CI)
yarn workspace @vocab-bloom-hub/client build           # dist/ (ESM, CJS, d.ts)
yarn workspace @vocab-bloom-hub/client test            # unit tests + the client against the real server on SQLite
yarn workspace @vocab-bloom-hub/client pack:check      # publint + arethetypeswrong on the packed tarball (CI, release)

The package ships ESM and CommonJS with a declaration file for each (dist/index.d.ts, dist/index.d.cts); the exports map hands every consumer the pair its resolution asks for.

src/generated/openapi.ts is produced by openapi-typescript from the committed public spec and committed itself: a contract change on the server shows up as a diff here, and test/contract.spec.ts fails until every native /api/v1 operation of the spec has a client method. Compatibility adapters have generated types but no wrapper methods: they are intended for third-party clients replacing their base URL.

License

MIT for the client. The dictionary data comes under the license of the dataset the instance serves — CC BY 4.0 for the project's own, the license of the source for another, the one its owner chose for a dataset of the instance's own: read license, attribution and license_text from meta() and show them with the data (title names the dataset). An entry the owner of the instance changed carries modified: true, and client.history(headword) says what was changed: say next to such an entry that it was changed. wordDatasets(headword) answers from every dataset of the instance: each group carries the terms of its own dataset, and entries taken from several groups are bound by each of them.

A word may have several origins, contributions and licenses. Each license's origin_id refers to its source or contribution snapshot: preserve the name, version, links, attribution and notices, and respect license_relation (all or any). For example, OpenGloss words marked as derived from WordNet retain both sets of terms. An unchanged fork keeps the original terms; actual edits add the fork's captured contribution terms. The primary license from meta() does not replace these word-specific licenses. These fields are optional when reading older v1 servers. See the API source fields.

What the licenses ask for: DATA_LICENSE.md.

Search selected dictionaries

const choices = await client.datasets(); // public names and terms; installation order
const flat = await client.search({ search: 'run', dataset: 'wiktionary' });
const detailed = await client.searchDetailed({ search: 'run', dataset: 'wiktionary', with_meanings: true });
const grouped = await client.searchDatasets({ search: 'run', datasets: ['default', 'wiktionary'], limit: 5 });
for (const group of grouped.data) {
  if (group.status === 'ok') console.log(group.dataset, group.data, group.meta.has_more);
  else console.log(group.dataset, group.error); // unavailable, not an empty match
}

Omit dataset to keep active-dictionary behavior. Explicit unknown or closed targets fail with 404, without fallback. Grouped search accepts 1–5 names; each costs one unit of the shared public budget. Pagination and ranking are per group; request only that name to advance one group. Both selected searches and inventory revalidate on every read. Partial-failure responses are not cached. Preserve each group's source terms and use its dataset when linking to a word page.