npmjs-api-client
v1.10.2
Published
TypeScript client for the npm Registry REST API
Maintainers
Readme
npmjs-api-client
TypeScript client for the npm ecosystem. Aggregates data from multiple sources into a single, chainable API — package metadata, download stats, install size, CDN usage, file contents, and resolved dependency graphs. Works in Node.js and the browser (isomorphic). Fully typed, zero runtime dependencies.
Data sources integrated:
| Source | What it provides | | --- | --- | | registry.npmjs.org | Package metadata, versions, dist-tags, search, maintainers, authenticated user/org data | | api.npmjs.org | Download counts by period, per-day breakdown, version-level stats | | packagephobia.com | Publish size and full install size including all transitive dependencies | | data.jsdelivr.com/v1 | CDN browser usage — hits by version and file | | unpkg.com | Full file tree with individual file sizes and paths | | api.deps.dev/v3 | Fully resolved dependency graph with exact versions |
Installation
npm install npmjs-api-clientQuick start
import { NpmClient } from 'npmjs-api-client';
// Public APIs — no auth required
const npm = new NpmClient();
// Private registry with auth token
const privateNpm = new NpmClient({
registryUrl: 'https://my-registry.example.com',
token: 'my-auth-token',
});
// Authenticated npm registry APIs
const authenticatedNpm = new NpmClient({ token: 'npm_...' });
// Custom base URLs per data source
const customNpm = new NpmClient({
packagephobiaUrl: 'https://packagephobia.com',
jsdelivrUrl: 'https://data.jsdelivr.com/v1',
unpkgUrl: 'https://unpkg.com',
depsDevUrl: 'https://api.deps.dev/v3',
});The
tokenis always sent to the registry, and to the downloads API only when it shares the registry's origin or both are npm's defaults — a private registry token never reachesapi.npmjs.org. It is never sent to third-party sources.
API reference
Package metadata
// Full packument — all versions, dist-tags, maintainers, readme, etc.
const pkg = await npm.package('typescript');
const pkg = await npm.package('typescript').get(); // same
// All published versions as an ordered array
const versions = await npm.package('typescript').versions();
console.log(versions.map(v => v.version).join(', '));
// Current maintainers
const maintainers = await npm.package('typescript').maintainers();
maintainers.forEach(m => console.log(m.name, m.email));
// Dist-tags
const tags = await npm.package('typescript').distTags();
// { latest: '6.0.2', beta: '6.0.0-beta', rc: '6.0.1-rc', ... }Version manifest
// Specific version
const manifest = await npm.package('typescript').version('5.4.5');
const manifest = await npm.package('typescript').version('5.4.5').get(); // same
// Latest version (shorthand for .version('latest'))
const latest = await npm.package('typescript').latest();
console.log(manifest.version); // '5.4.5'
console.log(manifest.license); // 'Apache-2.0'
console.log(manifest.dist.tarball); // tarball URL
console.log(manifest.dependencies); // semver ranges from package.jsonDownload stats
// Total downloads over a period
const stats = await npm.package('typescript').downloads(); // last-month (default)
const stats = await npm.package('typescript').downloads('last-day');
const stats = await npm.package('typescript').downloads('last-week');
const stats = await npm.package('typescript').downloads('last-year');
const stats = await npm.package('typescript').downloads('2024-01-01:2024-03-31');
console.log(stats.downloads); // 163781296
console.log(stats.start); // '2024-01-01'
console.log(stats.end); // '2024-01-31'
// Per-day breakdown
const range = await npm.package('typescript').downloadRange('last-month');
range.downloads.forEach(d => console.log(d.day, d.downloads));
// Version-level downloads (last-week only)
const versionStats = await npm.package('typescript').version('5.4.5').downloads('last-week');
console.log(versionStats.downloads);
// Convenience methods on the client directly
const stats = await npm.downloads('last-week', 'typescript');
const range = await npm.downloadRange('last-month', 'typescript');
// Bulk downloads — unscoped packages are batched (128 per request),
// scoped packages are fetched one request each
const bulk = await npm.bulkDownloads(['react', 'vue', '@angular/core']);
const bulk = await npm.bulkDownloads(['react', 'vue'], 'last-week');
console.log(bulk['react']?.downloads); // 18591460
console.log(bulk['vue']?.downloads); // 4200000
console.log(bulk['@angular/core']?.downloads); // 1800000
// Packages that do not exist map to nullQuality score (deprecated)
score()is deprecated. npm no longer computes quality, popularity, or maintenance scores. Its search index now returns1for every metric inscore.detail, andscore.finalis the search relevance of the package name — an unbounded number, not a 0–1 score. npms.io, which used to provide a detailed per-metric breakdown, has also been discontinued. There is no public replacement.For popularity signals, use download counts, or the
downloadsanddependentsfields of search results.
const score = await npm.package('react').score();
console.log(score.score.final); // 2467.3 — search relevance, not quality
console.log(score.score.detail.quality); // 1 — always 1
console.log(score.analyzedAt); // '2024-01-01T00:00:00.000Z'Install size — Packagephobia
Returns the published tarball size and the full installation footprint including all transitive dependencies.
// Latest version
const size = await npm.package('react').size();
// Specific version
const size = await npm.package('react').version('18.2.0').size();
// Publish size (the tarball itself)
console.log(size.publish.bytes); // 12400
console.log(size.publish.files); // 10
console.log(size.publish.pretty); // '12.4 kB'
console.log(size.publish.color); // 'green'
// Install size (package + all transitive dependencies)
console.log(size.install.bytes); // 307200
console.log(size.install.files); // 35
console.log(size.install.pretty); // '307 kB'
console.log(size.install.color); // 'green' ('yellow' > 1 MB, 'red' > 10 MB)CDN usage stats — jsDelivr
Measures actual browser requests from production apps — complementary to npm download counts, which reflect install-time usage.
// Package-level stats (grouped by version by default, last month)
const stats = await npm.package('react').cdnStats();
console.log(stats.rank); // 1
console.log(stats.total); // 1234567890
// Grouped by date instead
const byDate = await npm.package('react').cdnStats('date', 'week');
// Per-version breakdown
Object.entries(stats.versions ?? {}).forEach(([ver, data]) => {
console.log(`${ver}: ${data.total} hits`);
});
// Version-level stats (grouped by file by default)
const versionStats = await npm.package('react').version('18.2.0').cdnStats();
console.log(versionStats.total);
Object.entries(versionStats.files ?? {}).forEach(([file, data]) => {
console.log(`${file}: ${data.total} hits`);
});Available periods: 'day' | 'week' | 'month' (default) | 'year'
Available groupBy at package level: 'version' (default) | 'date'
Available groupBy at version level: 'file' (default) | 'date'
File tree — unpkg
Lists every file included in a published version with individual sizes, types, and paths.
const tree = await npm.package('react').version('18.2.0').files();
console.log(tree.type); // 'directory'
// Recursively list all files
function listFiles(node: UnpkgFile, indent = 0) {
const pad = ' '.repeat(indent);
if (node.type === 'file') {
console.log(`${pad}${node.path} (${node.size} bytes)`);
} else {
console.log(`${pad}${node.path}/`);
node.files?.forEach(f => listFiles(f, indent + 2));
}
}
listFiles(tree);
// Check if types are included
const hasTypes = tree.files?.some(f => f.path.endsWith('.d.ts'));Resolved dependency graph — deps.dev
Returns exact resolved versions for every dependency in the tree — not the semver ranges from package.json, but what actually installs.
const deps = await npm.package('react').version('18.2.0').dependencies();
// All nodes in the graph
deps.nodes.forEach(n => {
console.log(`${n.relation}: ${n.versionKey.name}@${n.versionKey.version}`);
// 'SELF': [email protected]
// 'DIRECT': [email protected]
// 'INDIRECT': [email protected]
});
// Direct dependencies only
const direct = deps.nodes.filter(n => n.relation === 'DIRECT');
// Bundled dependencies (embedded in the tarball, not installed separately)
const bundled = deps.nodes.filter(n => n.bundled);
// Resolution errors (broken or unresolvable dependencies)
const broken = deps.nodes.filter(n => n.errors.length > 0);
// Dependency edges (who requires whom, and with which constraint)
deps.edges.forEach(e => {
const from = deps.nodes[e.fromNode].versionKey.name;
const to = deps.nodes[e.toNode].versionKey.name;
console.log(`${from} → ${to} (${e.requirement})`);
});Security audit
Runs a security audit against the npm registry. The payload mirrors the top-level structure of package-lock.json.
const payload = {
name: 'my-app',
version: '1.0.0',
requires: { lodash: '^4.17.11' },
dependencies: {
lodash: { version: '4.17.11', integrity: 'sha512-...' },
},
};
// Full audit — advisory details + recommended actions
const result = await npm.audit(payload);
console.log(result.metadata.vulnerabilities);
// { info: 0, low: 0, moderate: 0, high: 1, critical: 0 }
Object.values(result.advisories).forEach(a => {
console.log(`[${a.severity}] ${a.title}`);
console.log(` module: ${a.module_name}@${a.vulnerable_versions}`);
console.log(` fix: upgrade to ${a.patched_versions}`);
console.log(` url: ${a.url}`);
});
result.actions.forEach(action => {
console.log(`${action.action}: ${action.module} → ${action.target}`);
});
// Quick audit — counts only, no advisory details (faster)
const quick = await npm.auditQuick(payload);
const { high, critical } = quick.metadata.vulnerabilities;
if (high + critical > 0) {
console.error(`${high} high and ${critical} critical vulnerabilities found`);
}Both methods support AbortSignal and emit a request event with method: 'POST'.
Search
const results = await npm.search({ text: 'typescript client' });
const results = await npm.search({ text: 'react hooks', size: 10, from: 0 });
results.objects.forEach(o => {
console.log(
o.package.name,
o.package.version,
o.downloads?.weekly, // downloads in the last 7 days
o.dependents, // number of dependent packages, as a string: '216179'
);
});
console.log(results.total); // total matches
// Rank the results by popularity yourself
const byDownloads = [...results.objects].sort(
(a, b) => (b.downloads?.weekly ?? 0) - (a.downloads?.weekly ?? 0),
);
// Top package shortcuts
const top = await npm.topPackages(10);
const types = await npm.topByKeyword('typescript', 10);
const scoped = await npm.topByScope('@types', 10);npm ranks search results by text relevance only. The
quality,popularity, andmaintenanceweights ofsearch()andmaintainer().packages()are ignored by npm and are deprecated.topByPopularity(),topByQuality(), andtopByMaintenance()are deprecated too: they return the same results astopPackages().
Maintainer
// Public profile
const user = await npm.maintainer('sindresorhus').info();
console.log(user.name, user.email);
// Avatar URL derived from the maintainer's public email
const avatarUrl = await npm.maintainer('sindresorhus').avatar();
// 'https://www.gravatar.com/avatar/...'
// All packages maintained (paginated)
const result = await npm.maintainer('sindresorhus').packages();
console.log(`${result.total} packages`);
result.objects.forEach(o => console.log(o.package.name, o.package.version));
// Second page
const page2 = await npm.maintainer('sindresorhus').packages({ size: 25, from: 25 });User
Authenticated user endpoints require an npm registry token.
const npm = new NpmClient({ token: 'npm_...' });
// Validate a token and discover which account it belongs to
const { username } = await npm.whoami();
console.log(username); // 'pilmee'
// Registry user profile document
const profile = await npm.user('pilmee').get();
console.log(profile.name, profile.email);
// Package names associated with the user
const packages = await npm.user('pilmee').packages();
packages.forEach(name => console.log(name));
// Paginate when the registry supports it
const page2 = await npm.user('pilmee').packages({ size: 25, from: 25 });Organization
const npm = new NpmClient({ token: 'npm_...' });
// Packages the org can access
const packages = await npm.org('npmcli').packages();
console.log(packages['@npmcli/arborist']); // 'read-write'
// Teams in the org
const teams = await npm.org('npmcli').teams();
teams.forEach(team => console.log(team)); // 'npmcli:developers'
// Org members and roles
const members = await npm.org('npmcli').members();
console.log(members.isaacs); // 'owner'
// Members in a specific team
const developers = await npm.org('npmcli').teamMembers('developers');
developers.forEach(username => console.log(username));Chainable package resource pattern
Package resources implement PromiseLike, so you can await them directly or chain methods:
// Await directly → fetches the packument
const pkg = await npm.package('typescript');
// Chain → fetches the version manifest
const manifest = await npm.package('typescript').version('5.4.5');
// Chain → latest version
const latest = await npm.package('typescript').latest();Cancelling requests
Pass an AbortSignal to any method to cancel the in-flight request:
const controller = new AbortController();
setTimeout(() => controller.abort(), 3000);
await npm.package('react').get(controller.signal);
await npm.package('react').score(controller.signal);
await npm.package('react').size(controller.signal);
await npm.package('react').cdnStats('version', 'month', controller.signal);
await npm.package('react').version('18.2.0').files(controller.signal);
await npm.package('react').version('18.2.0').dependencies(controller.signal);
await npm.package('react').downloads('last-week', controller.signal);
await npm.search({ text: 'react' }, controller.signal);
await npm.maintainer('sindresorhus').packages({}, controller.signal);
await npm.user('pilmee').get(controller.signal);
await npm.user('pilmee').packages({}, controller.signal);
await npm.bulkDownloads(['react', 'vue'], 'last-week', controller.signal);
await npm.audit(payload, controller.signal);
await npm.auditQuick(payload, controller.signal);
await npm.whoami(controller.signal);When aborted, fetch throws a DOMException with name === 'AbortError'. The request event is still emitted with the error attached.
Request events
Subscribe to every HTTP request for logging, monitoring, or debugging:
npm.on('request', (event) => {
console.log(`[${event.method}] ${event.url} → ${event.statusCode} (${event.durationMs}ms)`);
if (event.error) {
console.error('Request failed:', event.error.message);
}
});| Field | Type | Description |
| --- | --- | --- |
| url | string | Full URL that was requested |
| method | 'GET' \| 'POST' | HTTP method used |
| startedAt | Date | When the request started |
| finishedAt | Date | When the request finished |
| durationMs | number | Duration in milliseconds |
| statusCode | number \| undefined | HTTP status code, if a response was received |
| error | Error \| undefined | Present only if the request failed |
Remove a listener with off(), passing the same function given to on():
const log = (event: RequestEvent) => console.log(event.url);
npm.on('request', log);
npm.off('request', log);Multiple listeners can be registered. Listener exceptions and rejected promises are isolated: they do not change the request result or prevent other listeners from running. Async listeners are not awaited. Each listener is responsible for logging or handling its own errors. The event is always emitted after the request completes, whether it succeeded or failed. Events are emitted for all data sources — registry, downloads, packagephobia, jsDelivr, unpkg, and deps.dev.
Error handling
Non-2xx responses throw an NpmApiError with the HTTP status code, status text, and the response body. When the body explains the failure, detail holds that reason and it is appended to message:
import { NpmApiError } from 'npmjs-api-client';
try {
await npm.search({ text: 'a' });
} catch (err) {
if (err instanceof NpmApiError) {
console.log(err.status); // 400
console.log(err.statusText); // '' — often empty, see below
console.log(err.detail); // "The 'text' parameter must be between 2 and 64 characters"
console.log(err.body); // { error: "The 'text' parameter ...", code: 'ERR_TEXT_LENGTH' }
console.log(err.message); // "npm API error: 400 — The 'text' parameter ..."
}
}| Field | Type | Description |
| --- | --- | --- |
| status | number | HTTP status code |
| statusText | string | HTTP status text; often empty, since many servers (and HTTP/2) do not send one |
| body | unknown | Parsed JSON body, or raw text when the body is not JSON |
| detail | string \| undefined | Reason extracted from the body, when there is one |
TypeScript types
All domain types are exported:
import type {
// Client
NpmClientOptions, RequestEvent, NpmClientEvents,
// Package
NpmPackument, NpmPerson, NpmRepository,
NpmPackageVersion, NpmDist,
NpmDistTags,
// Search
NpmSearchResult, NpmSearchObject, NpmSearchPackage,
NpmSearchParams, NpmScore, NpmScoreDetail, NpmPackageLinks,
// User
NpmUser, NpmAuthenticatedUser, NpmUserPackages, NpmUserPackagesParams, NpmWhoami,
// Downloads
NpmDownloadPoint, NpmDownloadRange, NpmDownloadDay, NpmDownloadPeriod,
NpmVersionDownloadPeriod, NpmVersionDownloadPoint, NpmBulkDownloads,
// Score (evaluation types are deprecated — npms.io was discontinued)
NpmsScore, NpmsScoreDetail, NpmsEvaluation,
NpmsQualityEvaluation, NpmsPopularityEvaluation, NpmsMaintenanceEvaluation,
// Packagephobia
PackagephobiaSize, PackagephobiaSizeInfo,
// jsDelivr
JsdelivrStats, JsdelivrVersionEntry, JsdelivrPeriod, JsdelivrGroupBy,
// unpkg
UnpkgFile,
// deps.dev
DepsDevDependencies, DepsDevDependencyNode, DepsDevDependencyEdge, DepsDevVersionKey,
// Audit
NpmAuditPayload, NpmAuditDependency,
NpmAuditResult, NpmAuditQuickResult,
NpmAuditAdvisory, NpmAuditAction, NpmAuditFinding,
NpmAuditMetadata, NpmAuditVulnerabilityCounts, NpmAuditSeverity,
} from 'npmjs-api-client';Documentation
Full API documentation is published at: https://eljijuna.github.io/npmjs-api-client
Contributing
See CONTRIBUTING.md.
