Trendflow JS
A type-safe JavaScript/TypeScript library for querying and exporting Google Trends data.
The JavaScript port of trendflow-py.
๐ Documentation: trendflow.mory.dev/docs/js โ guides for both libraries, plus a hosted MCP server for ChatGPT, Claude, and Cursor.
- GitHub: https://github.com/dariomory/trendflow-js/
- npm package: https://www.npmjs.com/package/trendflow
- API reference: https://dariomory.github.io/trendflow-js/
- Python sibling: https://pypi.org/project/trendflow-py/
- Created by: Dario Mory | GitHub https://github.com/dariomory
- Free software: MIT License
Install
npm install trendflow
Requires Node.js 18+ (uses the global fetch). Ships ESM and CommonJS with bundled type declarations.
Usage
import { Client, Region, Timeframe, Resolution, ExportFormat } from "trendflow";
// Initialize client (optional config)
const tf = new Client({ language: "en", timeout: 10_000 });
// --- Const objects for type safety ---
// Region.US, Region.GB, Region.DE ...
// Timeframe.PAST_DAY, Timeframe.PAST_WEEK, Timeframe.PAST_YEAR, Timeframe.PAST_5_YEARS
// Resolution.COUNTRY, Resolution.REGION, Resolution.CITY
// Fetch interest over time
const data = await tf.interestOverTime(
["Python", "JavaScript", "Rust"],
Timeframe.PAST_YEAR,
Region.US,
);
console.log(data.keywords); // ["Python", "JavaScript", "Rust"]
console.log(data.granularity); // "weekly"
console.log(data.points); // TrendPoint[] โ { date: Date, scores: Record<string, number> }
// Regional breakdown (region defaults to Region.US)
const regional = await tf.interestByRegion("Python", Resolution.COUNTRY);
for (const row of regional.rows) {
console.log(row.label, row.value);
}
// Trending searches right now (any country code, or omit for worldwide)
const trending = await tf.trendingNow(Region.US);
for (const item of trending.results) {
console.log(item.title, item.growth, item.volume, item.traffic);
// "fifa world cup 2026" 3650 6 "+3,650%"
}
// Related queries
const related = await tf.relatedQueries("machine learning");
for (const query of related.top) console.log(query.term, query.value);
for (const query of related.rising) console.log(query.term, query.breakout);
// --- Exports ---
data.toArray(); // [{ date: Date, Python: 80, ... }] โ plain objects, the JS answer to DataFrames
data.toJSON(); // same rows with ISO 8601 date strings (also drives JSON.stringify)
data.toCSV(); // CSV text
// Node.js only โ writes UTF-8 to disk
await data.export(ExportFormat.CSV, "trends.csv");
await data.export(ExportFormat.JSON, "trends.json");
Errors
Failed requests throw ResponseError, or TooManyRequestsError (a subclass) on HTTP 429.
Both carry .status and the raw .response.
import { TooManyRequestsError } from "trendflow";
try {
await tf.interestOverTime(["Python"], Timeframe.PAST_YEAR, Region.US);
} catch (error) {
if (error instanceof TooManyRequestsError) {
// Google is rate-limiting this IP โ back off and retry later.
}
}
Trending backends: RPC and RSS
Google exposes trending searches two ways. They are not interchangeable, so backend lets
you pick:
"rpc" (batchexecute) | "rss" (feed) | |
|---|---|---|
| items | 50 | 10 |
| payload | ~2 KB JSON | ~21 KB XML |
| growth % and volume | โ | โ โ buckets like "2000+" |
| news articles | โ | โ |
window selection | โ | ignored by Google |
| worldwide | โ | โ country only |
const rss = await tf.trendingNow(Region.US, { backend: "rss" });
rss.source; // "rss"
rss.results[0].articles;
// [{ title: "...", url: "https://...", source: "Buffalo News", picture: "https://..." }]
"auto" (the default) tries the RPC and falls back to the feed. The RPC comes first
deliberately: it returns five times the items with real growth figures, so defaulting to RSS
would quietly degrade results. Reach for "rss" when you want the articles โ that is the
one thing the RPC cannot give you โ or as a second opinion if the RPC id ever goes stale.
Note that the feed is not a lighter path despite being a feed, and Google ignores hours,
sort and count on it: it always returns the same 10 entries.
Topics and search suggestions
Google distinguishes a search term (the literal string) from a topic (the entity, in
every spelling and language). suggestions() finds the topic; every query method already
accepts one โ pass the mid where you would pass a keyword.
const topics = await tf.suggestions("artificial intelligence");
// [{ mid: "/m/0mkz", title: "Artificial intelligence", type: "Professional field" }]
const data = await tf.interestOverTime(
[topics[0].mid, "artificial intelligence"],
Timeframe.PAST_YEAR,
Region.US,
);
// { "/m/0mkz": 62, "artificial intelligence": 1 }
That gap is the point: the topic scores 62 where the literal phrase scores 1, because it aggregates every phrasing and translation people actually search.
suggestions() needs no cookie and no proxy โ it answers on IPs the widgetdata endpoints
reject with 429, same as trendingNow(). type disambiguates same-name entities
("Nike" returns both the company and the goddess) and is null when Google omits it.
Rate limits
Google Trends aggressively rate-limits datacenter and shared IPs, so 429 is common even on
your first request of the day. Two things matter:
- User-Agent. Google returns
429to the default agent strings Node HTTP clients send, no matter how few requests you have made. This library sends a browser User-Agent by default for exactly that reason โ if you overrideheaders, keep a realistic one. - IP reputation. Once an IP is flagged, every request gets
429regardless of headers. Route through a residential proxy to recover.
Using a proxy pool
Pass a list of proxy URLs and the client rotates through them automatically, moving to the next one whenever a query is refused:
import { Client, Region, Timeframe } from "trendflow";
const tf = new Client({
proxies: [
"http://user:pass@gate.decodo.com:7000",
"http://user:pass@gate.decodo.com:7000",
],
maxProxyAttempts: 3, // defaults to the pool size, capped at 5
onProxyRotate: ({ attempt, error }) => console.warn(`rotated after ${attempt}:`, error),
});
const data = await tf.interestOverTime(["Python"], Timeframe.PAST_YEAR, Region.US);
console.log(tf.currentProxy); // the proxy that answered
Proxy support needs undici, an optional peer
dependency โ npm install undici. Entries are just URLs, so a pool can mix providers. Repeating one rotating gateway also works: each entry gets its own connection, so it lands on a fresh exit IP.
Rotation happens per query, not per request โ this matters. Google binds the NID
cookie and the widget token to the IP that requested them, so a single query must complete
on one exit IP; sending the follow-up widgetdata call from a different IP earns an instant
429. The pool pins one proxy for the whole query and advances only on failure, re-seeding
the cookie jar each time. For the same reason, point the pool at sticky sessions rather
than per-request rotating endpoints if your provider offers the choice.
Rotation is skipped for errors a different IP cannot fix, such as a 404 or the
UnknownRpcError raised when Google renames a batchexecute RPC id.
Where to get proxies
Residential proxies are what actually clears Google's 429. Verified against this library:
| Provider | Notes | Endpoint format |
|---|---|---|
| Decodo (formerly Smartproxy) | Cheapest entry tier; pay-as-you-go available. Used to verify this library's live tests. | http://user:pass@gate.decodo.com:7000 |
const tf = new Client({
proxies: ["http://user:pass@gate.decodo.com:7000"],
});
Ask for sticky sessions when you sign up โ per-request rotating endpoints break the
cookie/token binding described above. Note that a shared residential pool can be exhausted
for Google Trends specifically, in which case even a valid proxy returns 429; that is what
maxProxyAttempts is for.
Bringing your own client
For logging, caching, or custom routing, pass a fetch instead (mutually exclusive with
proxies โ the library will tell you if you pass both):
import { ProxyAgent, fetch as undiciFetch } from "undici";
const agent = new ProxyAgent("http://user:pass@proxy.example.com:7000");
const tf = new Client({
fetch: ((input, init = {}) =>
undiciFetch(input, { ...init, dispatcher: agent })) as typeof globalThis.fetch,
});
Browser / Next.js
Every method except export() works anywhere fetch does, but Google Trends sends no CORS
headers โ calls from browser JavaScript will be blocked. Use this library server-side
(Route Handlers, Server Actions, API routes) and pass results to the client.
MCP server
An MCP server ships alongside the library as trendflow-mcp, so agents can query
Google Trends directly. It's a separate package โ the library keeps its zero runtime
dependencies.
claude mcp add trendflow -- npx -y trendflow-mcp
Six tools (search_topics, get_interest_over_time, get_interest_by_region,
get_related_queries, get_trending_now, research_trend) and two resources. See
mcp/README.md.
Feature Parity
Current: trendflow-py 0.2.0 ยท trendflow 0.1.0. Versions are independent; each changelog cross-references the sibling release.
| Feature | Python โ trendflow-py | JS โ trendflow |
|---|---|---|
| Interest over time | โ | โ |
| Interest by region | โ | โ |
| Trending now | โ | โ |
| Trending growth % and volume | โ | โ |
| Trending for any country code | โ | โ |
| Trending news articles (RSS) | โ | โ |
| Selectable trending backend | โ | โ |
| Related queries | โ | โ |
| Search suggestions | โ
suggestions() | โ
suggestions() |
| Query by topic (entity mid) | โ | โ |
| CSV / JSON export | โ | โ |
| Rotating proxy pool | โ | โ |
| Browser User-Agent by default | โ | โ |
| Full geo hierarchy | โ
geo_list() | โ
geoList() |
| Overridable RPC ids | โ | โ |
| pandas DataFrame | โ
to_dataframe() | โ N/A |
| Plain-object rows | โ N/A | โ
toArray() |
| ESM + CommonJS + types | โ N/A | โ |
| MCP server | ๐ planned | โ
trendflow-mcp |
| CLI | โ | ๐ planned |
Trending now
Google retired the hottrends/visualize/internal/data endpoint, along with
api/dailytrends and api/realtimetrends; all three now return HTTP 404. This library
calls the batchexecute RPC that trends.google.com itself uses instead โ as does
trendflow-py from 0.2.0 โ and it returns more
than the old endpoint did:
const trending = await tf.trendingNow(Region.US);
// { title: "fifa world cup 2026", growth: 3650, volume: 6, traffic: "+3,650%", articles: [] }
Three practical wins over the old endpoint:
- Growth and volume, not just titles.
growthis the percentage rise over the window,volumea relative search-volume index. - Any country code, not the 16 hardcoded names the old endpoint required โ and worldwide works, which it previously refused.
- No cookie, and far looser rate limiting. This RPC answers on IPs that get a
429from the widgetdata endpoints, sotrendingNow()often works with no proxy at all.
articles is empty on this backend โ the RPC carries no article links. Pass
{ backend: "rss" } to get the news articles behind each trend instead.
The window is selectable via TrendingWindow:
import { TrendingWindow } from "trendflow";
await tf.trendingNow(Region.US, { window: TrendingWindow.RISING }); // default: fastest-growing
await tf.trendingNow(Region.US, { window: TrendingWindow.TOP }); // highest-volume
window is an undocumented Google parameter. Only these two values have behaviour worth
naming; other integers between 4 and 12 also return data over varying recency windows, and
you can pass one as a raw number.
Not implemented: captcha-gated RPCs
The same batchexecute endpoint exposes a higher-precision timeseries (floating-point
values rather than the rounded 0-100 the public API returns) and keyword-scoped related
queries. Both require a reCAPTCHA Enterprise token and return an empty payload without one,
so this library does not implement them โ that data remains available through
interestOverTime() and relatedQueries(), which use the documented widgetdata endpoints.
API mapping
| Python | JavaScript |
|---|---|
interest_over_time() | interestOverTime() |
interest_by_region() | interestByRegion() |
trending_now() | trendingNow() |
related_queries() | relatedQueries() |
to_dataframe() | toArray() |
export(fmt, path) | export(fmt, path) โ Node only, plus toCSV() / toJSON() |
Notable differences:
- Everything is async. All four query methods return promises.
timeoutis milliseconds (JS convention), not seconds.- Enums are
as constobjects, soRegion.USis the string"US"and any valid string literal is accepted where the type is expected. - Results are plain typed objects. Only
InterestOverTimeResultis a class, because it carries the conversion methods; the rest are interfaces.
Development
git clone git@github.com:dariomory/trendflow-js.git
cd trendflow-js
npm install
npm test # vitest โ 60 tests, fully offline against a stubbed fetch
npm run qa # typecheck + test + build
The unit tests never touch the network. To check the real endpoints:
npm run build && npm run smoke
TRENDFLOW_PROXY_URL=http://user:pass@host:7000 npm run smoke # via a proxy
Author
Trendflow JS was created in 2026 by Dario Mory.