{
  "markdown": "<p align=\"center\">\n  <img src=\"docs/logo.png\" alt=\"Trendflow JS logo\" width=\"300\"/>\n</p>\n\n# Trendflow JS\n\n[![npm version](https://img.shields.io/npm/v/trendflow.svg)](https://www.npmjs.com/package/trendflow)\n[![CI](https://github.com/dariomory/trendflow-js/actions/workflows/ci.yml/badge.svg)](https://github.com/dariomory/trendflow-js/actions/workflows/ci.yml)\n[![docs](https://img.shields.io/badge/docs-trendflow.mory.dev-0b6e6e)](https://trendflow.mory.dev/docs/js)\n[![trendflow-js MCP server](https://glama.ai/mcp/servers/dariomory/trendflow-js/badges/score.svg)](https://glama.ai/mcp/servers/dariomory/trendflow-js)\n\nA type-safe JavaScript/TypeScript library for querying and exporting Google Trends data.\nThe JavaScript port of [`trendflow-py`](https://github.com/dariomory/trendflow).\n\n📖 **Documentation: [trendflow.mory.dev/docs/js](https://trendflow.mory.dev/docs/js)** — guides for\nboth libraries, plus a hosted MCP server for ChatGPT, Claude, and Cursor.\n\n- GitHub: [https://github.com/dariomory/trendflow-js/](https://github.com/dariomory/trendflow-js/)\n- npm package: [https://www.npmjs.com/package/trendflow](https://www.npmjs.com/package/trendflow)\n- API reference: [https://dariomory.github.io/trendflow-js/](https://dariomory.github.io/trendflow-js/)\n- Python sibling: [https://pypi.org/project/trendflow-py/](https://pypi.org/project/trendflow-py/)\n- Created by: **[Dario Mory](https://mory.dev)** | GitHub [https://github.com/dariomory](https://github.com/dariomory)\n- Free software: MIT License\n\n## Install\n\n```bash\nnpm install trendflow\n```\n\nRequires Node.js 18+ (uses the global `fetch`). Ships ESM and CommonJS with bundled type declarations.\n\n## Usage\n\n```ts\nimport { Client, Region, Timeframe, Resolution, SearchProperty, ExportFormat } from \"trendflow\";\n\n// Initialize client (optional config)\nconst tf = new Client({ language: \"en\", timeout: 10_000 });\n\n// --- Const objects for type safety ---\n// Region.US, Region.GB, Region.DE ...           (or any code: \"US-CA\", \"807\")\n// Timeframe.PAST_HOUR ... PAST_5_YEARS, ALL_TIME (or \"2023-01-01 2023-06-30\")\n// Resolution.COUNTRY, Resolution.REGION, Resolution.CITY\n// SearchProperty.WEB, IMAGES, NEWS, YOUTUBE, SHOPPING\n\n// Fetch interest over time\nconst data = await tf.interestOverTime(\n  [\"Python\", \"JavaScript\", \"Rust\"],\n  Timeframe.PAST_YEAR,\n  Region.US,\n);\n\nconsole.log(data.keywords);    // [\"Python\", \"JavaScript\", \"Rust\"]\nconsole.log(data.granularity); // \"weekly\"\nconsole.log(data.points);      // TrendPoint[] — { date: Date, scores: Record<string, number> }\n\n// Regional breakdown (region defaults to Region.US)\nconst regional = await tf.interestByRegion(\"Python\", Resolution.COUNTRY);\nfor (const row of regional.rows) {\n  console.log(row.label, row.value);\n}\n\n// Trending searches right now (any country code, or omit for worldwide)\nconst trending = await tf.trendingNow(Region.US);\nfor (const item of trending.results) {\n  console.log(item.title, item.growth, item.volume, item.traffic);\n  // \"fifa world cup 2026\"  3650  6  \"+3,650%\"\n}\n\n// Related queries (region defaults to worldwide)\nconst related = await tf.relatedQueries(\"machine learning\", { region: Region.GB });\nfor (const query of related.top) console.log(query.term, query.value);\nfor (const query of related.rising) console.log(query.term, query.breakout);\n\n// --- Narrowing a query ---\n// Every query method takes an optional category and search property, and any of them\n// accepts a custom date range and a sub-region or metro code in place of the named values.\n\n// \"jaguar\" the car, on YouTube, in California, over the first half of 2023\nconst jaguar = await tf.interestOverTime([\"jaguar\"], \"2023-01-01 2023-06-30\", \"US-CA\", {\n  category: 47, // Autos & Vehicles — disambiguates without needing a topic id\n  searchProperty: SearchProperty.YOUTUBE,\n});\n\n// --- Exports ---\ndata.toArray();  // [{ date: Date, Python: 80, ... }] — plain objects, the JS answer to DataFrames\ndata.toJSON();   // same rows with ISO 8601 date strings (also drives JSON.stringify)\ndata.toCSV();    // CSV text\n\n// Node.js only — writes UTF-8 to disk\nawait data.export(ExportFormat.CSV, \"trends.csv\");\nawait data.export(ExportFormat.JSON, \"trends.json\");\n```\n\n### Errors\n\nFailed requests throw `ResponseError`, or `TooManyRequestsError` (a subclass) on HTTP 429.\nBoth carry `.status` and the raw `.response`.\n\n```ts\nimport { TooManyRequestsError } from \"trendflow\";\n\ntry {\n  await tf.interestOverTime([\"Python\"], Timeframe.PAST_YEAR, Region.US);\n} catch (error) {\n  if (error instanceof TooManyRequestsError) {\n    // Google is rate-limiting this IP — back off and retry later.\n  }\n}\n```\n\n### Trending backends: RPC and RSS\n\nGoogle exposes trending searches two ways. They are not interchangeable, so `backend` lets\nyou pick:\n\n| | `\"rpc\"` (`batchexecute`) | `\"rss\"` (feed) |\n|---|---|---|\n| items | 50 | 10 |\n| payload | ~2 KB JSON | ~21 KB XML |\n| growth % and volume | ✅ | ❌ — buckets like `\"2000+\"` |\n| news articles | ❌ | ✅ |\n| `window` selection | ✅ | ignored by Google |\n| worldwide | ✅ | ❌ country only |\n\n```ts\nconst rss = await tf.trendingNow(Region.US, { backend: \"rss\" });\nrss.source; // \"rss\"\nrss.results[0].articles;\n// [{ title: \"...\", url: \"https://...\", source: \"Buffalo News\", picture: \"https://...\" }]\n```\n\n`\"auto\"` (the default) tries the RPC and falls back to the feed. The RPC comes first\ndeliberately: it returns five times the items with real growth figures, so defaulting to RSS\nwould quietly degrade results. Reach for `\"rss\"` when you want the **articles** — that is the\none thing the RPC cannot give you — or as a second opinion if the RPC id ever goes stale.\n\nNote that the feed is not a lighter path despite being a feed, and Google ignores `hours`,\n`sort` and `count` on it: it always returns the same 10 entries.\n\n### Topics and search suggestions\n\nGoogle distinguishes a **search term** (the literal string) from a **topic** (the entity, in\nevery spelling and language). `suggestions()` finds the topic; every query method already\naccepts one — pass the `mid` where you would pass a keyword.\n\n```ts\nconst topics = await tf.suggestions(\"artificial intelligence\");\n// [{ mid: \"/m/0mkz\", title: \"Artificial intelligence\", type: \"Professional field\" }]\n\nconst data = await tf.interestOverTime(\n  [topics[0].mid, \"artificial intelligence\"],\n  Timeframe.PAST_YEAR,\n  Region.US,\n);\n// { \"/m/0mkz\": 62, \"artificial intelligence\": 1 }\n```\n\nThat gap is the point: the topic scores **62** where the literal phrase scores **1**, because\nit aggregates every phrasing and translation people actually search.\n\n`suggestions()` needs no cookie and no proxy — it answers on IPs the widgetdata endpoints\nreject with `429`, same as `trendingNow()`. `type` disambiguates same-name entities\n(`\"Nike\"` returns both the company and the goddess) and is `null` when Google omits it.\n\n<a id=\"rate-limits\"></a>\n### Rate limits\n\nGoogle Trends aggressively rate-limits datacenter and shared IPs, so `429` is common even on\nyour first request of the day. Two things matter:\n\n1. **User-Agent.** Google returns `429` to the default agent strings Node HTTP clients send,\n   no matter how few requests you have made. This library sends a browser User-Agent by\n   default for exactly that reason — if you override `headers`, keep a realistic one.\n2. **IP reputation.** Once an IP is flagged, every request gets `429` regardless of headers.\n   Route through a residential proxy to recover.\n\n### Using a proxy pool\n\nPass a list of proxy URLs and the client rotates through them automatically, moving to the\nnext one whenever a query is refused:\n\n```ts\nimport { Client, Region, Timeframe } from \"trendflow\";\n\nconst tf = new Client({\n  proxies: [\n    \"http://user:pass@gate.decodo.com:7000\",\n    \"http://user:pass@gate.decodo.com:7000\",\n  ],\n  maxProxyAttempts: 3, // defaults to the pool size, capped at 5\n  onProxyRotate: ({ attempt, error }) => console.warn(`rotated after ${attempt}:`, error),\n});\n\nconst data = await tf.interestOverTime([\"Python\"], Timeframe.PAST_YEAR, Region.US);\nconsole.log(tf.currentProxy); // the proxy that answered\n```\n\nProxy support needs [`undici`](https://github.com/nodejs/undici), an optional peer\ndependency — `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.\n\n**Rotation happens per query, not per request — this matters.** Google binds the `NID`\ncookie and the widget token to the IP that requested them, so a single query must complete\non one exit IP; sending the follow-up `widgetdata` call from a different IP earns an instant\n`429`. The pool pins one proxy for the whole query and advances only on failure, re-seeding\nthe cookie jar each time. For the same reason, point the pool at **sticky sessions** rather\nthan per-request rotating endpoints if your provider offers the choice.\n\nRotation is skipped for errors a different IP cannot fix, such as a `404` or the\n`UnknownRpcError` raised when Google renames a `batchexecute` RPC id.\n\n#### Where to get proxies\n\nResidential proxies are what actually clears Google's `429`. Verified against this library:\n\n<p align=\"center\">\n  <a href=\"https://dashboard.decodo.com/register?referral_code=821058adf31e1b797a169971f79daf86fd5ebbbc\"><img src=\"docs/proxies/decodo.svg\" alt=\"Decodo\" height=\"56\"/></a>\n</p>\n\n| Provider | Notes | Endpoint format |\n|----------|-------|-----------------|\n| [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` |\n\n```ts\nconst tf = new Client({\n  proxies: [\"http://user:pass@gate.decodo.com:7000\"],\n});\n```\n\nAsk for **sticky sessions** when you sign up — per-request rotating endpoints break the\ncookie/token binding described above. Note that a shared residential pool can be exhausted\nfor Google Trends specifically, in which case even a valid proxy returns `429`; that is what\n`maxProxyAttempts` is for.\n\n#### Bringing your own client\n\nFor logging, caching, or custom routing, pass a `fetch` instead (mutually exclusive with\n`proxies` — the library will tell you if you pass both):\n\n```ts\nimport { ProxyAgent, fetch as undiciFetch } from \"undici\";\n\nconst agent = new ProxyAgent(\"http://user:pass@proxy.example.com:7000\");\nconst tf = new Client({\n  fetch: ((input, init = {}) =>\n    undiciFetch(input, { ...init, dispatcher: agent })) as typeof globalThis.fetch,\n});\n```\n\n### Browser / Next.js\n\nEvery method except `export()` works anywhere `fetch` does, but Google Trends sends no CORS\nheaders — calls from browser JavaScript will be blocked. Use this library server-side\n(Route Handlers, Server Actions, API routes) and pass results to the client.\n\n## MCP server\n\nAn MCP server ships alongside the library as [`trendflow-mcp`](./mcp), so agents can query\nGoogle Trends directly. It's a separate package — the library keeps its zero runtime\ndependencies.\n\n```bash\nclaude mcp add trendflow -- npx -y trendflow-mcp\n```\n\nSix tools (`search_topics`, `get_interest_over_time`, `get_interest_by_region`,\n`get_related_queries`, `get_trending_now`, `research_trend`) and two resources. See\n[mcp/README.md](./mcp/README.md).\n\n## Feature Parity\n\nCurrent: [`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.\n\n| Feature | Python — [`trendflow-py`](https://pypi.org/project/trendflow-py/) | JS — [`trendflow`](https://www.npmjs.com/package/trendflow) |\n|---------|:----------------------------------:|:---------------------------:|\n| Interest over time | ✅ | ✅ |\n| Interest by region | ✅ | ✅ |\n| Trending now | ✅ | ✅ |\n| Trending growth % and volume | ✅ | ✅ |\n| Trending for any country code | ✅ | ✅ |\n| Trending news articles (RSS) | ✅ | ✅ |\n| Selectable trending backend | ✅ | ✅ |\n| Related queries | ✅ | ✅ |\n| Search suggestions | ✅ `suggestions()` | ✅ `suggestions()` |\n| Query by topic (entity mid) | ✅ | ✅ |\n| CSV / JSON export | ✅ | ✅ |\n| Rotating proxy pool | ✅ | ✅ |\n| Browser User-Agent by default | ✅ | ✅ |\n| Full geo hierarchy | ✅ `geo_list()` | ✅ `geoList()` |\n| Overridable RPC ids | ✅ | ✅ |\n| pandas DataFrame | ✅ `to_dataframe()` | ❌ N/A |\n| Plain-object rows | ❌ N/A | ✅ `toArray()` |\n| ESM + CommonJS + types | ❌ N/A | ✅ |\n| MCP server | 🔜 planned | ✅ [`trendflow-mcp`](https://www.npmjs.com/package/trendflow-mcp) |\n| CLI | ✅ | 🔜 planned |\n\n### Trending now\n\nGoogle retired the `hottrends/visualize/internal/data` endpoint, along with\n`api/dailytrends` and `api/realtimetrends`; all three now return HTTP 404. This library\ncalls the `batchexecute` RPC that trends.google.com itself uses instead — as does\n[`trendflow-py`](https://github.com/dariomory/trendflow) from 0.2.0 — and it returns more\nthan the old endpoint did:\n\n```ts\nconst trending = await tf.trendingNow(Region.US);\n// { title: \"fifa world cup 2026\", growth: 3650, volume: 6, traffic: \"+3,650%\", articles: [] }\n```\n\nThree practical wins over the old endpoint:\n\n- **Growth and volume**, not just titles. `growth` is the percentage rise over the window,\n  `volume` a relative search-volume index.\n- **Any country code**, not the 16 hardcoded names the old endpoint required — and\n  worldwide works, which it previously refused.\n- **No cookie, and far looser rate limiting.** This RPC answers on IPs that get a `429`\n  from the widgetdata endpoints, so `trendingNow()` often works with no proxy at all.\n\n`articles` is empty on this backend — the RPC carries no article links. Pass\n`{ backend: \"rss\" }` to get the news articles behind each trend instead.\n\nThe window is selectable via `TrendingWindow`:\n\n```ts\nimport { TrendingWindow } from \"trendflow\";\n\nawait tf.trendingNow(Region.US, { window: TrendingWindow.RISING }); // default: fastest-growing\nawait tf.trendingNow(Region.US, { window: TrendingWindow.TOP });    // highest-volume\n```\n\n`window` is an undocumented Google parameter. Only these two values have behaviour worth\nnaming; other integers between 4 and 12 also return data over varying recency windows, and\nyou can pass one as a raw number.\n\n#### Not implemented: captcha-gated RPCs\n\nThe same `batchexecute` endpoint exposes a higher-precision timeseries (floating-point\nvalues rather than the rounded 0-100 the public API returns) and keyword-scoped related\nqueries. Both require a reCAPTCHA Enterprise token and return an empty payload without one,\nso this library does not implement them — that data remains available through\n`interestOverTime()` and `relatedQueries()`, which use the documented widgetdata endpoints.\n\n### API mapping\n\n| Python                  | JavaScript             |\n|-------------------------|------------------------|\n| `interest_over_time()`  | `interestOverTime()`   |\n| `interest_by_region()`  | `interestByRegion()`   |\n| `trending_now()`        | `trendingNow()`        |\n| `related_queries()`     | `relatedQueries()`     |\n| `to_dataframe()`        | `toArray()`            |\n| `export(fmt, path)`     | `export(fmt, path)` — Node only, plus `toCSV()` / `toJSON()` |\n\nNotable differences:\n\n- **Everything is async.** All four query methods return promises.\n- **`timeout` is milliseconds** (JS convention), not seconds.\n- **Enums are `as const` objects**, so `Region.US` is the string `\"US\"` and any valid\n  string literal is accepted where the type is expected.\n- **Results are plain typed objects.** Only `InterestOverTimeResult` is a class, because it\n  carries the conversion methods; the rest are interfaces.\n\n## Development\n\n```bash\ngit clone git@github.com:dariomory/trendflow-js.git\ncd trendflow-js\nnpm install\n\nnpm test        # vitest — 60 tests, fully offline against a stubbed fetch\nnpm run qa      # typecheck + test + build\n```\n\nThe unit tests never touch the network. To check the real endpoints:\n\n```bash\nnpm run build && npm run smoke\nTRENDFLOW_PROXY_URL=http://user:pass@host:7000 npm run smoke   # via a proxy\n```\n\n## Author\n\nTrendflow JS was created in 2026 by Dario Mory.\n",
  "bytes": 16142,
  "sha": "efe8a2114e47cee68ab9350298bbbb3d8fe533d461df60586e57eb59cbe5f088",
  "repo_slug": "dariomory/trendflow-js",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_dev_mory_trendflow_968c9b9d/readme"
}