trendsearch
v0.1.0
Published
Google Trends API fetching library for Node.js and Bun.
Maintainers
Readme
trendsearch 📈
Modern Google Trends SDK for Node.js and Bun, built with native fetch, strict Zod validation, and a production-friendly client API.
✨ Highlights
- 🔒 Strict schema validation by default (Zod-backed)
- 🧠 Full TypeScript-first API and exported inferred types
- ⚡ Native
fetchtransport (Node 20+ and Bun) - 🧱 ESM-only package contract
- 🛡️ Built-in retry/backoff + rate limiting (
p-retry+p-queue) - 🍪 Optional cookie persistence support
- 🖥️ First-class
trendsearchCLI for every endpoint - 🌐 Stable Google Trends API endpoints + experimental RPC/picker/CSV/top charts endpoints
- 🧪 Deterministic fixture contracts + optional live endpoint tests
📦 Install
Install as a library dependency
bun add trendsearch
npm install trendsearch
pnpm add trendsearch
yarn add trendsearchRun the CLI without installing globally (always latest)
npx trendsearch@latest --help
pnpm dlx trendsearch@latest --help
yarn dlx trendsearch@latest --help
bunx trendsearch@latest --helpInstall the CLI globally (@latest)
Use @latest so your global binary is always installed from the newest published version.
npm install --global trendsearch@latest
pnpm add --global trendsearch@latest
bun add --global trendsearch@latest
yarn global add trendsearch@latestFor Yarn Berry/Modern, prefer yarn dlx trendsearch@latest (shown above) instead of global install.
Update an existing global install to latest
Re-run your package manager's global install command with @latest.
✅ Runtime Contract
- 🟢 Node.js
>=20 - 🟢 Bun
>=1.3.9 - 🟢 ESM-only package
- 🔴 CommonJS
require("trendsearch")is intentionally unsupported
If you are in a CJS project, use dynamic import:
const trendsearch = await import("trendsearch");🚀 Library Usage Guide
1) Quick start with endpoint helpers
import { interestOverTime } from "trendsearch";
const result = await interestOverTime({
keywords: ["typescript"],
geo: "US",
time: "today 3-m",
});
console.log(result.data.timeline.length);2) Reuse shared defaults with createClient
import { createClient } from "trendsearch";
const client = createClient({
hl: "en-US",
tz: 0,
timeoutMs: 15_000,
});
const result = await client.relatedQueries({
keywords: ["typescript"],
geo: "US",
});
console.log(result.data.top.length);3) Use exported schemas/types when you need stricter boundaries
import { schemas, type ExploreRequest } from "trendsearch";
const request: ExploreRequest = {
keywords: ["typescript", "javascript"],
geo: "US",
};
const validated = schemas.exploreRequestSchema.parse(request);🖥️ CLI Usage Guide
trendsearch ships with a production-ready CLI that wraps all stable and
experimental endpoints.
1) Start with help
trendsearch --help
trendsearch explore --help2) Run common endpoint commands
trendsearch autocomplete typescript --output json
trendsearch explore typescript --geo US --time "today 3-m" --output pretty
trendsearch interest-over-time typescript --geo US --time "today 3-m"
trendsearch related-queries typescript --geo US --time "today 12-m"
trendsearch experimental trending-now --geo US --language en --hours 243) Pass request payloads with --input
--input accepts inline JSON, a JSON file path, or - for stdin.
trendsearch explore --input '{"keywords":["typescript"],"geo":"US"}' --output json
trendsearch explore --input ./requests/explore.json --output json
cat ./requests/explore.json | trendsearch explore --input - --output jsonCLI Output Modes
--output pretty(human-friendly, default in TTY)--output json(single JSON envelope, default outside TTY)--output jsonl(JSON per line)
Success envelope:
{
"ok": true,
"endpoint": "autocomplete",
"request": { "keyword": "typescript" },
"data": { "topics": [] },
"meta": {
"command": "autocomplete",
"durationMs": 120,
"timestamp": "2026-02-14T00:00:00.000Z",
"output": "json"
}
}Error envelope (json/jsonl):
{
"ok": false,
"error": {
"code": "TRANSPORT_ERROR",
"message": "Request failed",
"details": {},
"exitCode": 5
}
}4) Persist defaults, use wizard, and generate completion
trendsearch config set output json
trendsearch config set hl en-US
trendsearch config get output
trendsearch config list
trendsearch config unset hl
trendsearch config reset
trendsearch wizard
trendsearch completion zshConfig precedence:
flags > env > persisted config > defaults
Supported env vars include:
TRENDSEARCH_OUTPUTTRENDSEARCH_SPINNERTRENDSEARCH_HLTRENDSEARCH_TZTRENDSEARCH_BASE_URLTRENDSEARCH_TIMEOUT_MSTRENDSEARCH_MAX_RETRIESTRENDSEARCH_RETRY_BASE_DELAY_MSTRENDSEARCH_RETRY_MAX_DELAY_MSTRENDSEARCH_MAX_CONCURRENTTRENDSEARCH_MIN_DELAY_MSTRENDSEARCH_USER_AGENTTRENDSEARCH_CONFIG_DIR(override where persisted CLI config is stored)
🧭 API Surface
Stable Endpoints
autocompleteexploreinterestOverTimeinterestByRegionrelatedQueriesrelatedTopicstrendingNowtrendingArticlesdailyTrends(legacy compatibility)realTimeTrends(legacy compatibility)
Experimental Endpoints
experimental.trendingNowexperimental.trendingArticlesexperimental.geoPickerexperimental.categoryPickerexperimental.topChartsexperimental.interestOverTimeMultirangeexperimental.interestOverTimeCsvexperimental.interestOverTimeMultirangeCsvexperimental.interestByRegionCsvexperimental.relatedQueriesCsvexperimental.relatedTopicsCsvexperimental.hotTrendsLegacy
⚠️ Experimental endpoints are semver-minor unstable because Google can change internal RPC payloads.
ℹ️ dailyTrends and realTimeTrends are kept for compatibility and may throw EndpointUnavailableError if Google retires those legacy routes.
Operational Caveats (Important)
- Internal/undocumented Google Trends routes can throttle aggressively (
HTTP 429) even at low request volume. - Some route families can intermittently fail with
HTTP 400,401,404, or410depending on backend changes. - For production use, keep concurrency low, cache aggressively, and use longer backoff windows.
- Prefer the official Google Trends API (alpha) when available for long-lived production integrations.
- Related data endpoints can validly return empty lists for some keywords/time windows.
Reducing 429 in Practice
trendsearch automatically retries on 429, and when Google sends a Retry-After header,
the client respects that wait window before retrying.
Recommended profile for unstable/internal routes:
import { MemoryCookieStore, createClient } from "trendsearch";
const client = createClient({
timeoutMs: 30_000,
retries: {
maxRetries: 5,
baseDelayMs: 2_500,
maxDelayMs: 45_000,
},
rateLimit: {
maxConcurrent: 1,
minDelayMs: 5_000,
},
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
cookieStore: new MemoryCookieStore(),
});🧰 Client Configuration
Use createClient when you want shared runtime defaults and transport controls:
import { MemoryCookieStore, createClient } from "trendsearch";
const client = createClient({
timeoutMs: 15_000,
baseUrl: "https://trends.google.com",
hl: "en-US",
tz: 240,
retries: {
maxRetries: 3,
baseDelayMs: 500,
maxDelayMs: 8_000,
},
rateLimit: {
maxConcurrent: 1,
minDelayMs: 1_000,
},
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
cookieStore: new MemoryCookieStore(),
proxyHook: async ({ url, init }) => ({ url, init }),
});Default Client Values
timeoutMs:15000baseUrl:https://trends.google.comhl:en-UStz: host timezone offset (new Date().getTimezoneOffset())retries.maxRetries:3retries.baseDelayMs:500retries.maxDelayMs:8000rateLimit.maxConcurrent:1rateLimit.minDelayMs:1000
🧪 Request/Response Pattern
All endpoint calls return:
{
data: ..., // normalized typed payload
raw?: ... // included only when debugRawResponse=true
}Enable raw payload diagnostics per request:
const result = await client.explore(
{ keywords: ["typescript"], geo: "US" },
{ debugRawResponse: true }
);
console.log(result.raw);📚 Endpoint Usage
autocomplete
import { autocomplete } from "trendsearch";
const result = await autocomplete({ keyword: "typescri" });
console.log(result.data.topics);Input:
keyword(required)hl,tz(optional)
Output:
data.topics: topic list (mid,title,type)
explore
import { explore } from "trendsearch";
const result = await explore({
keywords: ["typescript", "javascript"],
geo: "US",
time: "today 12-m",
category: 0,
property: "",
});
console.log(result.data.widgets);Input:
keywords(required, array)geo(string or string[])time,category,property,hl,tz
Output:
data.widgets: exploration widgetsdata.comparisonItem: normalized comparison items used inreq
interestOverTime
import { interestOverTime } from "trendsearch";
const result = await interestOverTime({
keywords: ["typescript"],
geo: "US",
time: "today 3-m",
});
console.log(result.data.timeline);Output:
data.timeline: timeline points (time,value,formattedTime,isPartial, ...)
interestByRegion
import { interestByRegion } from "trendsearch";
const result = await interestByRegion({
keywords: ["typescript"],
geo: "US",
resolution: "REGION",
});
console.log(result.data.regions);Output:
data.regions: geo map entries (geoCode,geoName,value, ...)
relatedQueries
import { relatedQueries } from "trendsearch";
const result = await relatedQueries({
keywords: ["typescript"],
geo: "US",
});
console.log(result.data.top);
console.log(result.data.rising);Output:
data.topdata.risingvaluecan benumber | string("Breakout"-style upstream values are preserved)data.topanddata.risingmay be empty arrays for some requests
relatedTopics
import { relatedTopics } from "trendsearch";
const result = await relatedTopics({
keywords: ["typescript"],
geo: "US",
});
console.log(result.data.top);
console.log(result.data.rising);Output:
data.topdata.risingdata.topanddata.risingmay be empty arrays for some requests
dailyTrends
import { dailyTrends } from "trendsearch";
const result = await dailyTrends({
geo: "US",
category: "all",
});
console.log(result.data.days);
console.log(result.data.trends);Input:
geo(required)category,date,ns,hl,tz
Output:
data.days: day-grouped payloaddata.trends: flattened trend list
realTimeTrends
import { realTimeTrends } from "trendsearch";
const result = await realTimeTrends({
geo: "US",
category: "all",
});
console.log(result.data.stories);Input:
geo(required)category,fi,fs,ri,rs,sort,hl,tz
Output:
data.stories: story summaries
trendingNow
import { trendingNow } from "trendsearch";
const result = await trendingNow({
geo: "US",
language: "en",
hours: 24,
});
console.log(result.data.items[0]?.articleKeys);trendingArticles
import { trendingArticles } from "trendsearch";
const result = await trendingArticles({
articleKeys: [[1, "en", "US"]],
articleCount: 5,
});
console.log(result.data.articles);🧪 Experimental Endpoint Usage
experimental.trendingNow and experimental.trendingArticles are aliases of the stable root methods and remain available for backward compatibility.
experimental.geoPicker
import { experimental } from "trendsearch";
const result = await experimental.geoPicker({ hl: "en-US" });
console.log(result.data.items);experimental.categoryPicker
import { experimental } from "trendsearch";
const result = await experimental.categoryPicker({ hl: "en-US" });
console.log(result.data.items);experimental.topCharts
import { experimental } from "trendsearch";
const result = await experimental.topCharts({
date: 2024,
geo: "GLOBAL",
});
console.log(result.data.charts);
console.log(result.data.items);experimental.interestOverTimeMultirange
import { experimental } from "trendsearch";
const result = await experimental.interestOverTimeMultirange({
keywords: ["typescript"],
geo: "US",
time: "today 3-m",
});
console.log(result.data.timeline);CSV Variants (experimental.*Csv)
import { experimental } from "trendsearch";
const result = await experimental.relatedQueriesCsv({
keywords: ["typescript"],
geo: "US",
});
console.log(result.data.csv);experimental.interestOverTimeCsv, experimental.interestOverTimeMultirangeCsv,
experimental.interestByRegionCsv, experimental.relatedQueriesCsv, and
experimental.relatedTopicsCsv currently return raw CSV text (data.csv) in v1.
experimental.hotTrendsLegacy
import { experimental } from "trendsearch";
const result = await experimental.hotTrendsLegacy();
console.log(result.data.payload);🧾 Schemas and Types
All request/response schemas and inferred types are exported:
import {
schemas,
type InterestOverTimeRequest,
type InterestOverTimeResponse,
} from "trendsearch";
const req: InterestOverTimeRequest = {
keywords: ["typescript"],
};
const parsed = schemas.interestOverTimeResponseSchema.parse(rawPayload);schemas includes stable + experimental schema exports, plus z.
🚨 Errors
Typed errors:
TrendSearchErrorTransportErrorRateLimitErrorSchemaValidationErrorEndpointUnavailableErrorUnexpectedResponseError
Example:
import {
EndpointUnavailableError,
RateLimitError,
SchemaValidationError,
} from "trendsearch";
try {
await interestOverTime({ keywords: ["typescript"] });
} catch (error) {
if (error instanceof RateLimitError) {
console.error("Rate limited:", error.status);
console.error("Retry after ms:", error.retryAfterMs);
}
if (error instanceof SchemaValidationError) {
console.error("Schema drift:", error.issues);
}
if (error instanceof EndpointUnavailableError) {
console.error("Legacy endpoint unavailable:", error.replacements);
}
}🧪 Testing and Quality Gates
Run tests:
bun run test:unit
bun run test:contracts
bun run test:all
TRENDSEARCH_LIVE=1 bun run test:liveLive test notes:
- The live suite is intentionally slower (conservative pacing) to reduce
429. - Experimental routes are best-effort and can be unavailable depending on backend state.
Package checks:
bun run build
bun run check:package
bun run check:pack
bun run test:consumerFull local gate:
bun run check:all📼 Fixture Workflow
Record/update fixtures from live endpoints:
bun run fixtures:recordFixtures are stored under:
tests/fixtures/raw/*
Contract tests use those fixtures for deterministic CI.
🤖 CI Notes
CIworkflow runs deterministic tests and package quality checks.Live Endpointsworkflow (.github/workflows/live-endpoints.yml) runs nightly + manual and executestest:live.
🔁 Migration
Migrating from google-trends-api?
👉 See MIGRATION.md for method mapping and before/after examples.
🛠️ Development Scripts
bun run dev- watch buildbun run build- build distbun run typecheck- TypeScript checksbun run lint- format/lint checksbun run format- format/fixbun run changeset- add release note
📄 License
MIT
