trendsearch
v0.2.1
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, Effect Schema validation, and a production-friendly client API.
✨ Highlights
- 🔒 Strict schema validation by default (Effect Schema-backed)
- 🧠 Full TypeScript-first API and exported inferred types
- ⚡ Native
fetchtransport (Node 20+ and Bun) - 🧱 ESM-only package contract
- 🛡️ Built-in Effect-powered retry/backoff + rate limiting
- 🍪 Optional cookie persistence support
- 🖥️ First-class
trendsearchCLI for every endpoint - 🌐 Stable Google Trends API endpoints + experimental RPC/picker/CSV/RSS 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 - 📦 Promise API:
trendsearch - 🧬 Effect API:
trendsearch/effect
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 the native Effect API
import { Effect } from "effect";
import { createClient } from "trendsearch/effect";
const client = createClient({
hl: "en-US",
timeoutMs: 15_000,
});
const program = Effect.gen(function* () {
const result = yield* client.interestOverTime({
keywords: ["typescript"],
geo: "US",
});
return result.data.timeline.length;
});
console.log(await Effect.runPromise(program));Effect applications can also provide the client as a service:
import { Effect } from "effect";
import { TrendSearch } from "trendsearch/effect";
const program = Effect.gen(function* () {
const client = yield* TrendSearch;
return yield* client.relatedQueries({ keywords: ["typescript"] });
}).pipe(Effect.provide(TrendSearch.layer({ hl: "en-US" })));4) 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.Schema.decodeUnknownSync(schemas.exploreRequestSchema)(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 completions
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 --completions 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
autocompleteexploreinterestOverTimeinterestByRegionrelatedQueriesrelatedTopicstrendingNowtrendingArticles
Experimental Endpoints
experimental.trendingNowexperimental.trendingArticlesexperimental.trendingRssexperimental.geoPickerexperimental.categoryPickerexperimental.interestOverTimeMultirangeexperimental.interestOverTimeCsvexperimental.interestOverTimeMultirangeCsvexperimental.interestByRegionCsvexperimental.relatedQueriesCsvexperimental.relatedTopicsCsv
⚠️ Experimental endpoints are semver-minor unstable because Google can change internal RPC payloads.
The Promise API (trendsearch) and native Effect API (trendsearch/effect)
expose the same endpoint names. The main path resolves Promise results; the
Effect subpath returns lazy Effect.Effect values with the same output
envelopes.
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
}The Effect subpath succeeds with the same envelope:
import { Effect } from "effect";
import { interestOverTime } from "trendsearch/effect";
const result = await Effect.runPromise(interestOverTime({ keywords: ["typescript"] }));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.risingvalueis normalized as a numberdata.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
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 compatibility wrappers with the same request and response shapes as the stable root methods.
experimental.trendingRss
import { experimental } from "trendsearch";
const result = await experimental.trendingRss({ geo: "US" });
console.log(result.data.items[0]?.news);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.interestOverTimeMultirange
import { experimental } from "trendsearch";
const result = await experimental.interestOverTimeMultirange({
keyword: "typescript",
ranges: [
{ time: "2024-01-01 2024-02-01", geo: "US" },
{ time: "2024-03-01 2024-04-01", geo: "US" },
],
});
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.
🧾 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.Schema.decodeUnknownSync(schemas.interestOverTimeResponseSchema)(rawPayload);schemas includes stable + experimental schema exports, plus the Effect Schema module.
🚨 Errors
Typed errors:
TrendSearchErrorTransportErrorRateLimitErrorSchemaValidationErrorEndpointUnavailableError(reserved for endpoint shutdown/deprecation reporting)UnexpectedResponseError
Example:
import { RateLimitError, SchemaValidationError, UnexpectedResponseError } 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 UnexpectedResponseError) {
console.error("Unexpected upstream response:", error.message);
}
}Effect-native error handling:
import { Effect } from "effect";
import { interestOverTime } from "trendsearch/effect";
const program = interestOverTime({ keywords: ["typescript"] }).pipe(
Effect.catchTag("RateLimitError", (error) =>
Effect.succeed({
data: { timeline: [] },
raw: error,
}),
),
);🧪 Testing and Quality Gates
Tests run on Vitest with @effect/vitest helpers for Effect-powered code.
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 endpoint checks are manual/local via
TRENDSEARCH_LIVE=1 bun run test:live; they are intentionally excluded from deterministic PR CI.
🔁 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
