trendflow

trendflow

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

Category
Visit Server

README

<p align="center"> <img src="docs/logo.png" alt="Trendflow JS logo" width="300"/> </p>

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, SearchProperty, 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 ...           (or any code: "US-CA", "807")
// Timeframe.PAST_HOUR ... PAST_5_YEARS, ALL_TIME (or "2023-01-01 2023-06-30")
// Resolution.COUNTRY, Resolution.REGION, Resolution.CITY
// SearchProperty.WEB, IMAGES, NEWS, YOUTUBE, SHOPPING

// 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 (region defaults to worldwide)
const related = await tf.relatedQueries("machine learning", { region: Region.GB });
for (const query of related.top) console.log(query.term, query.value);
for (const query of related.rising) console.log(query.term, query.breakout);

// --- Narrowing a query ---
// Every query method takes an optional category and search property, and any of them
// accepts a custom date range and a sub-region or metro code in place of the named values.

// "jaguar" the car, on YouTube, in California, over the first half of 2023
const jaguar = await tf.interestOverTime(["jaguar"], "2023-01-01 2023-06-30", "US-CA", {
  category: 47, // Autos & Vehicles โ€” disambiguates without needing a topic id
  searchProperty: SearchProperty.YOUTUBE,
});

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

<a id="rate-limits"></a>

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:

<p align="center"> <a href="https://dashboard.decodo.com/register?referral_code=821058adf31e1b797a169971f79daf86fd5ebbbc"><img src="docs/proxies/decodo.svg" alt="Decodo" height="56"/></a> </p>

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

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

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured