Most "scrape Google" tutorials are Python. Fine, but half the products I touch are Node. This post is the Node.js version: one POST request, clean JSON back, no cheerio, no puppeteer.
Why not scrape Google's HTML directly? Because you'll spend your time fighting CAPTCHAs, consent walls, and weekly markup changes instead of building. A SERP API hands you the parsed results so your code stays boring and stable.
The request
The API I'm using here is SerpBase (https://api.serpbase.dev). Auth is a plain X-API-Key header, every endpoint takes a POST JSON body, and it runs Google Search, Images, News, Videos, and Maps behind one key. Node 18+ has global fetch, so there's no dependency to install:
const API_KEY = "your_api_key"; // from https://serpbase.dev/register
const BASE = "https://api.serpbase.dev";
async function googleSearch(query, { hl = "en", gl = "us", page = 1 } = {}) {
const resp = await fetch(`${BASE}/google/search`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": API_KEY },
body: JSON.stringify({ q: query, hl, gl, page, device: "default" }),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
const data = await googleSearch("nodejs async");
console.log(data.organic?.[0]);
That's the whole call. Two headers, a JSON body, and you get the SERP back.
What comes back
Every SerpBase endpoint returns the same top-level envelope, so logging and error handling stay consistent:
{
"status": 0,
"request_id": "req_01Hxxxx",
"elapsed_ms": 1420,
"credits_charged": 1,
"search_type": "search",
"query": "nodejs async",
"page": 1,
"organic": [
{
"rank": 1,
"title": "Node.js v22.x Documentation: Async Hooks",
"link": "https://nodejs.org/api/async_hooks.html",
"display_url": "nodejs.org",
"snippet": "The node:async_hooks module provides an API..."
}
]
}
Key fields:
-
status—0means success; anything else is an error, also returned as JSON. -
elapsed_ms— gateway latency, handy for logging. -
credits_charged— what the request cost after refund logic. -
organic— the results array withrank,title,link,display_url,snippet.
When Google shows them, the response also carries featured_snippet, people_also_ask, knowledge_graph, and related_searches — all already parsed.
Mapping it to a clean array
A common task: "give me the top 10 results as a plain list." One map does it:
const top10 = (data.organic ?? []).slice(0, 10).map((r) => ({
rank: r.rank,
title: r.title,
url: r.link,
snippet: r.snippet ?? "",
}));
console.table(top10);
Handling the fields that are optional
Not every result has every field — snippet, display_url, and sitelinks can be absent. Always use ?. and ?? rather than assuming:
const first = data.organic?.[0];
if (first) {
const snippet = first.snippet ?? "(no snippet)";
const sitelinks = first.sitelinks?.length ?? 0;
console.log(`${first.title} — ${sitelinks} sitelinks`);
}
This keeps the parser from throwing on a lean response.
Cost before you commit
SERP requests are metered per successful call. On SerpBase, /google/search, /google/news, and /google/videos cost 1 credit each; /google/images and both Maps endpoints cost 2. Failed dispatches and upstream timeouts are refunded automatically, so retries don't silently drain your balance.
For a side project, the pricing is refreshing: 100 free searches on signup (no card), and a $3 Starter Boost gets you 10,000 searches at the lowest rate. Standard packs start at $10 for 20k and never expire.
A tiny CLI wrapper
To make it reusable, wrap it in a one-liner CLI:
// search.mjs
const [,, query = "serp api"] = process.argv;
const data = await googleSearch(query);
for (const r of data.organic ?? []) {
console.log(`${r.rank}. ${r.title}\n ${r.link}`);
}
node search.mjs "nodejs async"
Where to go next
- Swap
hl/glto simulate other markets:{ hl: "de", gl: "de" }returns German Google. - Read the endpoint reference in the SerpBase documentation for Images, News, Videos, and Maps.
- If you want the agent route, SerpBase also ships an MCP server and a portable skill for Claude, Codex, Cursor, and opencode.
Run one request against your own key and inspect organic — the whole loop is shorter than the cheerio selector you were about to write.
Top comments (0)