TrendFlow
Google Trends data: interest over time, by region, related queries, and trending now.
Open source Repository Open in the app JSON README (API)
About
Google Trends data: interest over time, by region, related queries, and trending now.
Details
- Kind
- MCP servers
- Topic
- No topic detected
- Publisher
- dev.mory
- Origin
- official
- Category
- ferramentas
- Transport
- http
- Version
- 0.1.0
- Stars
- 2
- Last push
- 2026-09-07T08:41:12Z
- Repository state
- ativo
- Language
- TypeScript
- License
- MIT
- Added
- 2026-08-29 03:01:30
- Updated
- 2026-08-29 03:01:30
- Origin id
dev.mory/trendflow
README
<p align="center">
<img src="docs/logo.png" alt="Trendflow JS logo" width="300"/>
</p>
# Trendflow JS
[](https://www.npmjs.com/package/trendflow)
[](https://github.com/dariomory/trendflow-js/actions/workflows/ci.yml)
[](https://trendflow.mory.dev/docs/js)
[](https://glama.ai/mcp/servers/dariomory/trendflow-js)
A type-safe JavaScript/TypeScript library for querying and exporting Google Trends data.
The JavaScript port of [`trendflow-py`](https://github.com/dariomory/trendflow).
๐ **Documentation: [trendflow.mory.dev/docs/js](https://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/](https://github.com/dariomory/trendflow-js/)
- npm package: [https://www.npmjs.com/package/trendflow](https://www.npmjs.com/package/trendflow)
- API reference: [https://dariomory.github.io/trendflow-js/](https://dariomory.github.io/trendflow-js/)
- Python sibling: [https://pypi.org/project/trendflow-py/](https://pypi.org/project/trendflow-py/)
- Created by: **[Dario Mory](https://mory.dev)** | GitHub [https://github.com/dariomory](https://github.com/dariomory)
- Free software: MIT License
## Install
```bash
npm install trendflow
```
Requires Node.js 18+ (uses the global `fetch`). Ships ESM and CommonJS with bundled type declarations.
## Usage
```ts
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`.
```ts
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 |
```ts
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.
```ts
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:
```ts
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`](https://github.com/nodejs/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](https://dashboard.decodo.com/register?referral_code=821058adf31e1b797a169971f79daf86fd5ebbbc) (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` |
```ts
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):
```ts
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`](./mcp), so agents can query
Google Trends directly. It's a separate package โ the library keeps its zero runtime
dependencies.
```bash
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](./mcp/README.md).
## Feature Parity
Current: [`trendflow-py`](https://github.com/dariomory/trendflow) 0.2.0 ยท [`trendflow`](https://github.com/dariomory/trendflow-js) 0.1.0. Versions are independent; each changelog cross-references the sibling release.
| Feature | Python โ [`trendflow-py`](https://pypi.org/project/trendflow-py/) | JS โ [`trendflow`](https://www.npmjs.com/package/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`](https://www.npmjs.com/package/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`](https://github.com/dariomory/trendflow) from 0.2.0 โ and it returns more
than the old endpoint did:
```ts
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`:
```ts
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
```bash
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:
```bash
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.