Trendflow JS logo

# Trendflow JS [![npm version](https://img.shields.io/npm/v/trendflow.svg)](https://www.npmjs.com/package/trendflow) [![CI](https://github.com/dariomory/trendflow-js/actions/workflows/ci.yml/badge.svg)](https://github.com/dariomory/trendflow-js/actions/workflows/ci.yml) [![docs](https://img.shields.io/badge/docs-trendflow.mory.dev-0b6e6e)](https://trendflow.mory.dev/docs/js) [![trendflow-js MCP server](https://glama.ai/mcp/servers/dariomory/trendflow-js/badges/score.svg)](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 } // 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. ### 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:

Decodo

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