google-search-scraper-api 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +351 -0
  3. package/index.js +51 -0
  4. package/package.json +23 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wordstotech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,351 @@
1
+ # google-search-scraper-api
2
+
3
+ A Node.js client for scraping Google search results without owning the infrastructure. Wraps the ScrapingBee Google Search API behind a small, opinionated interface so you can ship a working SERP pipeline in an afternoon instead of a quarter.
4
+
5
+ ```bash
6
+ npm install google-search-scraper-api
7
+ ```
8
+
9
+ ```js
10
+ const { GoogleSearchScraper } = require('google-search-scraper-api');
11
+
12
+ const scraper = new GoogleSearchScraper({ apiKey: 'YOUR-API-KEY' });
13
+
14
+ const { organic_results } = await scraper.search({ query: 'best running shoes' });
15
+ console.log(organic_results[0]);
16
+ // { position: 1, title: '...', url: '...', description: '...' }
17
+ ```
18
+
19
+ Get an API key (1,000 free credits, no card) at [scrapingbee.com](https://www.scrapingbee.com/).
20
+
21
+
22
+ ## Why this package exists
23
+
24
+ I've shipped two SERP scrapers from scratch. Both worked for about six weeks.
25
+
26
+ The first ran on a pool of cheap datacenter IPs. It hit ~300 successful requests before Google started serving the consent wall, then the sorry page, then nothing. The second swapped in residential proxies, added a headless Chromium fleet behind a Redis queue, and survived longer — until Google rotated three SERP layout variants in two months and every CSS selector in the parsing layer broke at once.
27
+
28
+ By the third project I stopped pretending the maintenance cost was incidental. A managed google search scraper api is one HTTP call, one parser, no proxy budget, no headless browser fleet. This package is the thin wrapper I wish I'd had on day one: friendly options, real defaults, a few helpers for the patterns that come up every time you build SERP tooling.
29
+
30
+
31
+ ## What you get
32
+
33
+ - **Structured JSON for every result type** — organic results, ads, featured snippets, People Also Ask, related searches, knowledge panels, top stories, image packs. No HTML parsing on your side.
34
+ - **Geo-targeted SERPs** — country, language, device. Pass `country: 'de'` and you get Google.de from a German residential IP.
35
+ - **News, images, shopping, video, maps verticals** via a single `searchType` flag.
36
+ - **Pagination** that doesn't require remembering `start=10` increments.
37
+ - **Sensible camelCase options** — the underlying API uses `country_code`, `nb_results`, `search_type`; you don't have to.
38
+ - **Async-first** — every method returns a Promise, plays well with `Promise.all`, `p-limit`, queues, workers.
39
+
40
+
41
+ ## How it works
42
+
43
+ Every `.search()` call sends a GET request to:
44
+
45
+ ```
46
+ https://app.scrapingbee.com/api/v1/store/google?api_key=…&search=…
47
+ ```
48
+
49
+ On ScrapingBee's side that triggers:
50
+
51
+ 1. Routing the request through a residential or stealth proxy (configurable)
52
+ 2. Loading the SERP in a real headless browser
53
+ 3. Parsing the rendered page into a JSON SERP schema
54
+ 4. Returning the structured result
55
+
56
+ You pay credits per successful response. Failed requests (HTTP 500) aren't charged, so it's safe to retry. There's no caching layer — every call returns the live SERP, which is what you want for rank tracking and what you'll need to add yourself if you're polling the same query repeatedly.
57
+
58
+
59
+ ## API reference
60
+
61
+ ### `new GoogleSearchScraper({ apiKey, timeout })`
62
+
63
+ | Option | Type | Default | Description |
64
+ | -------- | ------ | -------- | ------------------------------------ |
65
+ | `apiKey` | string | required | Your ScrapingBee API key |
66
+ | `timeout`| number | `60000` | Request timeout in milliseconds |
67
+
68
+ ### `.search(options)`
69
+
70
+ | Option | Type | Description |
71
+ | ------------ | --------- | -------------------------------------------------------------------------------------------- |
72
+ | `query` | string | The search query (required) |
73
+ | `country` | string | ISO-2 country code: `us`, `gb`, `de`, `fr`, `jp`, etc. Determines proxy geo + Google domain. |
74
+ | `language` | string | UI / results language: `en`, `de`, `es`, `fr`, etc. |
75
+ | `device` | string | `desktop` or `mobile` |
76
+ | `page` | number | Page number, 1-based |
77
+ | `nbResults` | number | Results per page (10–100) |
78
+ | `searchType` | string | `classic` (default), `news`, `images`, `videos`, `shopping`, `maps` |
79
+ | `addHtml` | boolean | Include raw HTML in the response |
80
+ | `extra` | object | Any additional ScrapingBee parameter passed through verbatim |
81
+
82
+ Returns a parsed JSON object — see the response shape below.
83
+
84
+
85
+ ## Response shape
86
+
87
+ A successful call returns an object that looks like this (trimmed for clarity):
88
+
89
+ ```json
90
+ {
91
+ "search_metadata": {
92
+ "query": "best running shoes",
93
+ "url": "https://www.google.com/search?q=best+running+shoes",
94
+ "number_of_results": 412000000
95
+ },
96
+ "organic_results": [
97
+ {
98
+ "position": 1,
99
+ "title": "The 12 Best Running Shoes of 2026 - Runner's World",
100
+ "url": "https://www.runnersworld.com/...",
101
+ "description": "We tested hundreds of pairs..."
102
+ }
103
+ ],
104
+ "featured_snippet": {
105
+ "title": "...",
106
+ "description": "...",
107
+ "url": "..."
108
+ },
109
+ "people_also_ask": [
110
+ { "question": "...", "answer": "...", "url": "..." }
111
+ ],
112
+ "related_searches": ["best running shoes for flat feet", "..."],
113
+ "top_stories": [],
114
+ "ads": [],
115
+ "knowledge_graph": {}
116
+ }
117
+ ```
118
+
119
+ Fields are present only when Google actually rendered them for that query. Always null-check.
120
+
121
+
122
+ ## Use cases (with working code)
123
+
124
+ These are the four use cases I keep coming back to. Each one is a complete, runnable snippet — drop your API key in and run with `node example.js`.
125
+
126
+ ### 1. Rank tracking for an SEO dashboard
127
+
128
+ You have a list of keywords, you want to know where a domain ranks for each, refreshed daily.
129
+
130
+ ```js
131
+ const { GoogleSearchScraper } = require('google-search-scraper-api');
132
+
133
+ const scraper = new GoogleSearchScraper({ apiKey: process.env.SB_KEY });
134
+ const targetDomain = 'runnersworld.com';
135
+ const keywords = ['best running shoes', 'best trail running shoes', 'best marathon shoes'];
136
+
137
+ async function rankFor(query) {
138
+ const { organic_results } = await scraper.search({ query, country: 'us', nbResults: 100 });
139
+ const hit = organic_results.find(r => new URL(r.url).hostname.endsWith(targetDomain));
140
+ return hit ? hit.position : null;
141
+ }
142
+
143
+ for (const kw of keywords) {
144
+ console.log(kw, '→', await rankFor(kw));
145
+ }
146
+ ```
147
+
148
+ Two things worth noting from production:
149
+
150
+ - `nbResults: 100` matters. If you only fetch the first page, anything ranking past position 10 looks like "not ranking" — same bug ate a week of my dashboard's accuracy until I noticed.
151
+ - Polling daily? Add a 1–2 second jitter between calls. ScrapingBee handles concurrency on their side, but jittering also makes your own logs easier to debug.
152
+
153
+ ### 2. People Also Ask mining for content briefs
154
+
155
+ When I build a content brief I want every PAA question Google fires for the head term plus a couple of variants — they're the cleanest signal for what the SERP audience is actually asking.
156
+
157
+ ```js
158
+ const seeds = ['how to scrape google', 'google search scraping', 'scrape google search results'];
159
+ const questions = new Set();
160
+
161
+ for (const query of seeds) {
162
+ const { people_also_ask = [] } = await scraper.search({ query, country: 'us' });
163
+ for (const paa of people_also_ask) questions.add(paa.question);
164
+ }
165
+
166
+ console.log([...questions]);
167
+ ```
168
+
169
+ The PAA box re-expands when you click each question on the live SERP; the API returns the first wave. For deeper trees, query the unique questions you got back as new seeds and dedupe.
170
+
171
+ ### 3. Local SERP monitoring across regions
172
+
173
+ A client launches a product in five countries. They want to see what Google shows their users in each market on day one, day seven, day thirty.
174
+
175
+ ```js
176
+ const markets = [
177
+ { country: 'us', language: 'en' },
178
+ { country: 'gb', language: 'en' },
179
+ { country: 'de', language: 'de' },
180
+ { country: 'fr', language: 'fr' },
181
+ { country: 'jp', language: 'ja' },
182
+ ];
183
+
184
+ const query = 'noise cancelling headphones';
185
+
186
+ const snapshots = await Promise.all(
187
+ markets.map(m => scraper.search({ query, ...m, device: 'mobile' }).then(r => ({ ...m, top3: r.organic_results.slice(0, 3) })))
188
+ );
189
+
190
+ for (const s of snapshots) console.log(s.country, s.top3.map(r => r.url));
191
+ ```
192
+
193
+ Two production notes:
194
+
195
+ - `device: 'mobile'` is the right default for most international markets — global mobile share crossed desktop years ago and Google's mobile SERP differs in feature set.
196
+ - Don't fan out to 200 parallel requests on day one. Start with `Promise.all` for ~10–20, then move to a concurrency-limited queue (see use case 4) once you exceed your plan's concurrency cap.
197
+
198
+ ### 4. Bulk keyword scraping with controlled concurrency
199
+
200
+ You're feeding 5,000 keywords into a database. You don't want to fire them all at once, and you want retries on transient failures.
201
+
202
+ ```js
203
+ const pLimit = require('p-limit');
204
+ const fs = require('fs/promises');
205
+
206
+ const limit = pLimit(10); // tune to match your ScrapingBee plan's concurrency cap
207
+
208
+ async function withRetry(fn, tries = 3) {
209
+ for (let i = 0; i < tries; i++) {
210
+ try { return await fn(); }
211
+ catch (err) { if (i === tries - 1) throw err; await new Promise(r => setTimeout(r, 1000 * (i + 1))); }
212
+ }
213
+ }
214
+
215
+ async function scrapeAll(keywords) {
216
+ const tasks = keywords.map(kw => limit(() => withRetry(() => scraper.search({ query: kw, country: 'us' }))));
217
+ return Promise.all(tasks);
218
+ }
219
+
220
+ const keywords = (await fs.readFile('./keywords.txt', 'utf8')).split('\n').filter(Boolean);
221
+ const results = await scrapeAll(keywords);
222
+
223
+ await fs.writeFile('./results.json', JSON.stringify(results, null, 2));
224
+ ```
225
+
226
+ ScrapingBee's plan tiers cap concurrency at 10 / 50 / 100 / 200 depending on the plan — exceed it and the API returns a clear error. The `p-limit` value should match (or sit just below) your tier cap.
227
+
228
+ ### 5. News monitoring
229
+
230
+ ```js
231
+ const { news_results = [] } = await scraper.search({
232
+ query: '"product launch" "your brand"',
233
+ searchType: 'news',
234
+ country: 'us',
235
+ });
236
+
237
+ for (const item of news_results) {
238
+ console.log(item.date, item.source, item.title, item.url);
239
+ }
240
+ ```
241
+
242
+ The `searchType: 'news'` flag returns Google News results with timestamps and publication metadata, which is what you actually want for a brand-monitoring pipeline. The classic SERP's Top Stories box is shallower.
243
+
244
+ ### 6. SERPs as RAG retrieval
245
+
246
+ If you're building an LLM workflow that needs fresh web context — answering questions about events the model wasn't trained on, building a research agent, grounding a customer-support bot — a structured SERP is a cheap retrieval layer.
247
+
248
+ ```js
249
+ async function searchContext(query) {
250
+ const { organic_results, featured_snippet, people_also_ask } = await scraper.search({ query, country: 'us', nbResults: 20 });
251
+ return {
252
+ answer: featured_snippet?.description,
253
+ sources: organic_results.slice(0, 5).map(r => ({ title: r.title, url: r.url, snippet: r.description })),
254
+ related_questions: people_also_ask?.map(p => p.question) ?? [],
255
+ };
256
+ }
257
+
258
+ // Pass `searchContext` output into your prompt as a tool-call result
259
+ ```
260
+
261
+
262
+ ## Patterns you'll need eventually
263
+
264
+ ### Retry on transient failures
265
+
266
+ ScrapingBee doesn't charge credits on HTTP 500 — retrying is genuinely safe. Three tries with linear backoff (1s, 2s, 3s) covers almost every transient blip in practice.
267
+
268
+ ### Caching
269
+
270
+ There's no server-side cache. If you're polling the same query inside a tight window, cache responses client-side with a key like `${query}|${country}|${device}|${page}`. Redis with a 6–24 hour TTL works well for rank tracking; in-memory `Map` is fine for short scripts.
271
+
272
+ ### Saving to CSV
273
+
274
+ ```js
275
+ const { stringify } = require('csv-stringify/sync');
276
+ const rows = results.flatMap(r => r.organic_results.map(o => ({
277
+ query: r.search_metadata.query,
278
+ position: o.position,
279
+ title: o.title,
280
+ url: o.url,
281
+ })));
282
+ const csv = stringify(rows, { header: true });
283
+ ```
284
+
285
+ ### TypeScript
286
+
287
+ The package ships plain JavaScript with no `.d.ts` bundled (yet). You can type the responses by writing a minimal `SearchResponse` interface in your own project — only `organic_results`, `featured_snippet`, and `people_also_ask` are worth typing for most apps.
288
+
289
+
290
+ ## When this package is the wrong choice
291
+
292
+ Honest answer matters more than a feature list. Skip ScrapingBee (and this wrapper) if:
293
+
294
+ - **You need login-walled content.** ScrapingBee's terms prohibit scraping behind logins. LinkedIn private profiles, gated content, intranet pages — wrong tool.
295
+ - **You're scraping 50 queries a month.** A free residential proxy and `cheerio` will get you there; you don't need a managed API.
296
+ - **You need millisecond latency.** SERP scraping involves a headless browser render. Expect 3–8 seconds per request. Fine for batch jobs, wrong for interactive search-as-you-type.
297
+ - **You want to avoid all SaaS dependencies.** This is a wrapper around an external API; the API going down means your scraper goes down.
298
+
299
+
300
+ ## Cost expectations
301
+
302
+ You pay ScrapingBee per successful result, not per minute of CPU. The Google Search API costs 10 credits per "light" call and 15 credits per "normal" call as of 2026, so a plan giving you 250,000 credits a month covers roughly 16,000–25,000 SERP requests. Run the math against your keyword volume before scaling up — for most SEO tools the unit economics work; for very high-frequency tracking (every keyword every hour) they don't.
303
+
304
+ Pricing details and the latest credit costs live on [ScrapingBee's pricing page](https://www.scrapingbee.com/pricing/).
305
+
306
+
307
+ ## FAQ
308
+
309
+ ### Is it legal to scrape Google search results?
310
+
311
+ Public SERP data is generally legal to collect in most jurisdictions, particularly for SEO research, brand monitoring, and competitive analysis. Personal data, copyrighted content, and login-walled material are different categories — check your local regulations and Google's terms before scaling a project. ScrapingBee specifically prohibits post-login scraping in its terms of service.
312
+
313
+ ### Why not run my own headless browser?
314
+
315
+ You can. I have. The hidden cost isn't the browser, it's the proxy rotation, the CAPTCHA solving, the layout-change parsing patches, the bot-detection arms race, and the engineer-hours that go into all of it. If your scraping volume is under a few thousand requests a month, DIY is cheaper. Above that, a managed google search scraper api is cheaper than the labour to maintain a homegrown one.
316
+
317
+ ### Does this work in serverless runtimes (Lambda, Vercel, Cloudflare Workers)?
318
+
319
+ Yes — the only dependency is `axios`. Works in any Node.js 14+ environment. Cloudflare Workers needs Node compatibility enabled.
320
+
321
+ ### How do I scrape Google search results behind a specific location, not just country?
322
+
323
+ Pass `country` and `language`, then for finer geo-targeting use the `extra` option to pass ScrapingBee's `geo` parameter (postal code or city). City-level targeting requires premium proxies, which are billed at a higher credit cost.
324
+
325
+ ### What about Google's other surfaces — Maps, Shopping, Images?
326
+
327
+ `searchType: 'maps'`, `searchType: 'shopping'`, `searchType: 'images'`. Same response object, different result arrays (`local_results`, `shopping_results`, `image_results`).
328
+
329
+ ### How do I handle rate limits?
330
+
331
+ ScrapingBee returns a 429 if you exceed your plan's concurrency cap. The fix is to lower your concurrency, not slow down requests inside the cap — use `p-limit` or any queue library and set the limit to your plan's concurrency number.
332
+
333
+ ### Can I use this for Google Scholar, Google Trends, Google Flights?
334
+
335
+ This package targets the standard Google search verticals (classic, news, images, shopping, videos, maps). For Scholar and Trends, ScrapingBee has separate endpoints not exposed here yet — open an issue if you'd like them added.
336
+
337
+
338
+ ## Documentation
339
+
340
+ - [ScrapingBee Google Search API documentation](https://www.scrapingbee.com/documentation/google-api/)
341
+ - [ScrapingBee pricing](https://www.scrapingbee.com/pricing/)
342
+
343
+
344
+ ## License
345
+
346
+ MIT. See [LICENSE](LICENSE).
347
+
348
+
349
+ ## Disclaimer
350
+
351
+ This is an unofficial Node.js wrapper around ScrapingBee's Google Search API. It's not affiliated with ScrapingBee or Google. Compliance with Google's terms of service and applicable data-protection law is your responsibility as the operator of the scraper.
package/index.js ADDED
@@ -0,0 +1,51 @@
1
+ const axios = require('axios');
2
+
3
+ const ENDPOINT = 'https://app.scrapingbee.com/api/v1/store/google';
4
+
5
+ class GoogleSearchScraper {
6
+ constructor({ apiKey, timeout = 60000 } = {}) {
7
+ if (!apiKey) {
8
+ throw new Error('GoogleSearchScraper requires an apiKey');
9
+ }
10
+ this.apiKey = apiKey;
11
+ this.timeout = timeout;
12
+ }
13
+
14
+ async search({
15
+ query,
16
+ country,
17
+ language,
18
+ device,
19
+ page,
20
+ nbResults,
21
+ searchType,
22
+ addHtml,
23
+ extra = {},
24
+ } = {}) {
25
+ if (!query) {
26
+ throw new Error('search() requires a "query" parameter');
27
+ }
28
+
29
+ const params = {
30
+ api_key: this.apiKey,
31
+ search: query,
32
+ ...extra,
33
+ };
34
+
35
+ if (country) params.country_code = country;
36
+ if (language) params.language = language;
37
+ if (device) params.device = device;
38
+ if (page !== undefined) params.page = page;
39
+ if (nbResults !== undefined) params.nb_results = nbResults;
40
+ if (searchType) params.search_type = searchType;
41
+ if (addHtml !== undefined) params.add_html = addHtml ? 'true' : 'false';
42
+
43
+ const response = await axios.get(ENDPOINT, {
44
+ params,
45
+ timeout: this.timeout,
46
+ });
47
+ return response.data;
48
+ }
49
+ }
50
+
51
+ module.exports = { GoogleSearchScraper, ENDPOINT };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "google-search-scraper-api",
3
+ "version": "0.0.1",
4
+ "description": "Node.js client for scraping Google Search results using the ScrapingBee Google Search API.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "wordstotech",
10
+ "license": "MIT",
11
+ "homepage": "https://www.scrapingbee.com/documentation/google-api/",
12
+ "dependencies": {
13
+ "axios": "^1.7.0"
14
+ },
15
+ "engines": {
16
+ "node": ">=14"
17
+ },
18
+ "files": [
19
+ "index.js",
20
+ "README.md",
21
+ "LICENSE"
22
+ ]
23
+ }