npm Download Stats Scraper: Trends & Rankings avatar

npm Download Stats Scraper: Trends & Rankings

Pricing

from $0.37 / 1,000 series scrapeds

Go to Apify Store
npm Download Stats Scraper: Trends & Rankings

npm Download Stats Scraper: Trends & Rankings

Track npm download volume per package across day, week, month, year or a custom range. Daily series, peak days and share-of-total. No key, no proxy.

Pricing

from $0.37 / 1,000 series scrapeds

Rating

0.0

(0)

Developer

Arman Hossain

Arman Hossain

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

5 days ago

Last modified

Share

npm Download Stats Scraper: Download volume for any set of npm packages, totals, a daily series, average and peak day, and share of the set

Reads download volume for any set of npm packages straight from the official npm downloads API. You get totals, a day-by-day series, average per day, peak day and each package's share of the set you asked about.

This is the counts endpoint at api.npmjs.org/downloads, not the registry, so there's no key, no login, no proxy and no browser, just JSON. Unscoped packages are batched 128 at a time, so a 500-package comparison is four HTTP requests and finishes in a couple of seconds.

Agent skill: SKILL.md

https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/npm-download-stats-scraper.md

What you get

FieldWhat it holds
packagePackage name as npm reports it
period, granularityThe window you asked for, and whether a daily series was requested
start, end, daysActual window npm served, and its length in days
totalDownloadsDownloads across the whole window
dailySeries[{ day, downloads }], populated when granularity is range and empty for point
averagePerDaytotalDownloads / days, rounded
peakDay, peakDownloadsBusiest day in the series and its count, range only
shareOfComparisonSetFraction of the combined downloads of every package in the run, compareMode only
urlnpmjs.com package page
scrapedAtRun timestamp

Records are sorted by totalDownloads descending, so the dataset is already a ranking. RUN_SUMMARY in the key-value store holds counts, the request count, the filters you used, and any package that returned nothing.

Use cases

  • Library benchmarking. Compare your library against its alternatives in one run, with share of total included.
  • Adoption inflection points. Pull a year of daily data and diff week over week.
  • OSS traction reporting. Defensible numbers from the canonical source, on a schedule.
  • Supply-chain and SBOM triage. Weight dependency risk by how much the ecosystem actually uses a package.
  • DevRel dashboards. Feed a chart directly from the dataset, no transformation needed.

Quick start

Last month's totals for three frameworks:

{
"packages": ["express", "fastify", "koa"],
"period": "last-month",
"granularity": "point"
}

A year of daily data with share of total, for a competitive chart:

{
"packages": ["react", "vue", "svelte", "@angular/core"],
"period": "last-year",
"granularity": "range",
"compareMode": true
}

An explicit window, for the release quarter you actually care about:

{
"packages": ["zod", "yup", "joi", "valibot"],
"period": "2026-01-01:2026-06-30",
"granularity": "range",
"compareMode": true
}

Input

FieldTypeDefaultNotes
packagesarrayrequiredPackage names, scoped or plain. Duplicates are removed before any request.
periodstringlast-monthlast-day, last-week, last-month, last-year, YYYY-MM-DD, or YYYY-MM-DD:YYYY-MM-DD.
granularitystringrangepoint for a single total, range to also get the daily series.
compareModebooleanfalseFills in shareOfComparisonSet. Only meaningful with two or more packages.

compareMode compares the packages in this run, so the share is relative to the set you passed, not to all of npm. Combine it with granularity: "point" when you only want the ranking and want the dataset small.

Output example

A real record, produced by {"packages": ["express".], "period": "last-week", "granularity": "range", "compareMode": true}:

{
"package": "express",
"period": "last-week",
"granularity": "range",
"start": "2026-07-30",
"end": "2026-08-05",
"days": 7,
"totalDownloads": 130006647,
"dailySeries": [
{ "day": "2026-07-30", "downloads": 22186799 },
{ "day": "2026-07-31", "downloads": 19639130 },
{ "day": "2026-08-01", "downloads": 10836680 },
{ "day": "2026-08-02", "downloads": 10368617 },
{ "day": "2026-08-03", "downloads": 21441612 },
{ "day": "2026-08-04", "downloads": 22925745 },
{ "day": "2026-08-05", "downloads": 22608064 }
],
"averagePerDay": 18572378,
"peakDay": "2026-08-04",
"peakDownloads": 22925745,
"shareOfComparisonSet": 0.8015,
"url": "https://www.npmjs.com/package/express",
"scrapedAt": "2026-08-06T11:40:59.376Z"
}

The weekend dip visible in that series, 10.8M on Saturday against 22.9M on Tuesday, is normal for developer tooling and is exactly why range beats point for anything trend-shaped.

Choosing a period

You wantperiodgranularity
Yesterday's numberlast-daypoint
A weekly leaderboardlast-weekpoint
A trend chartlast-yearrange
A specific quarter2026-01-01:2026-03-31range
One historical day2026-07-01point

npm keeps roughly 18 months of daily history. Ask for more and it quietly moves start forward, so check the start field on the record rather than the period you sent.

API example

curl -X POST "https://api.apify.com/v2/acts/arman-bd~npm-download-stats-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"packages": ["express", "fastify", "koa"],
"period": "last-month",
"granularity": "point",
"compareMode": true
}'

JavaScript example

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/npm-download-stats-scraper').call({
packages: ['react', 'vue', 'svelte'],
period: 'last-year',
granularity: 'range',
compareMode: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const s of items) {
console.log(`${s.package}: ${s.totalDownloads.toLocaleString()} (${(s.shareOfComparisonSet * 100).toFixed(1)}%)`);
}

Limits and behaviour

  • Unscoped packages are batched, scoped ones are not. npm's bulk endpoint accepts up to 128 comma-separated names but rejects anything starting with @, so scoped packages get one request each. The Actor splits your list automatically and logs how many requests it made.
  • A missing package does not kill the run. Unknown names come back null inside a bulk response, or 404 on their own. Either way they land in RUN_SUMMARY.failures and everything else is still saved. The Actor errors only if nothing resolved at all.
  • Transient errors get retried. 429 and 5xx get three attempts with linear backoff. 400 and 404 are fatal for that batch, because a malformed period will never succeed.
  • Counts are downloads, not users. CI runners, mirrors and Docker builds all count. Treat the numbers as a relative signal over time, not as an installed base.
  • Public data only. No authentication, no personal data, nothing that gets around access controls.

FAQ

Do I need a proxy? No. Proxy configuration is not required to run this Actor.

Do I need an npm account or token? No. You supply no credentials.

How many packages can I pass? As many as you like. Unscoped names cost one request per 128, and scoped names cost one request each. A thousand unscoped packages is eight requests.

Why is a package missing from my dataset? Either the name is wrong, or it has never been published, or it has no downloads in the window. RUN_SUMMARY.failures names it explicitly.

How far back does history go? About 18 months of daily data. Longer ranges are trimmed by npm rather than rejected, so always read start and end from the record.

Does this give me versions and dependencies too? No, that is a different npm API. Use npm Package Scraper for registry metadata.

Can I schedule it? Yes, it is designed for it. Run daily with granularity: "point" and build your own history, or weekly with range for ready-made charts.