Back to Discover

trendflow-js

connector

dariomory

Google Trends data: interest over time, by region, related queries, and trending now.

View on GitHub
0 starsSynced Aug 16, 2026

Install to Claude Code

/plugin marketplace add dariomory/trendflow-js

README

Trendflow JS logo

Trendflow JS

npm version CI docs

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.

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)
items5010
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:

  1. User-Agent. Google returns 429 to 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 override headers, keep a realistic one.
  2. IP reputation. Once an IP is flagged, every request gets 429 regardless 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:

Decodo

ProviderNotesEndpoint 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.

FeaturePython โ€” trendflow-pyJS โ€” 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. growth is the percentage rise over the window, volume a 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 429 from the widgetdata endpoints, so trendingNow() 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

PythonJavaScript
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.
  • timeout is milliseconds (JS convention), not seconds.
  • Enums are as const objects, so Region.US is the string "US" and any valid string literal is accepted where the type is expected.
  • Results are plain typed objects. Only InterestOverTimeResult is 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.

Rendered live from dariomory/trendflow-js's GitHub README โ€” not stored, always reflects the source repo.

1 Install Method

NameDescriptionCategorySource
streamable-http remoteHosted streamable-http endpointmcp-serverhttps://trendflow.mory.dev/mcp

0 Comments

Login required
Log in to post a comment or update on this repo.

No comments yet โ€” be the first to share an update.