~ / guides / JavaScript & Node.js Web Scraping: The Complete Guide

JavaScript & Node.js Web Scraping: The Complete Guide

MR
Marcus Reed
Founder & lead tester · about the author
the short version
  • Node 18+ ships a built-in fetch, so the basic stack is fetch + Cheerio: download the HTML, parse it with jQuery-style selectors in about 15 lines.
  • Cheerio does not run JavaScript. For pages that build content in the browser, drive a real one with Playwright or Puppeteer.
  • I ran every script in this guide on Node v22.18.0 against the toscrape.com sandboxes. fetch + Cheerio pulled all 1,000 books across 50 pages; on the JS quotes build fetch found 0 quotes and Playwright found 10.
  • Parsing is the easy 15 lines. Staying unblocked across thousands of requests is the hard part, and that is the job a scraper API handles.

JavaScript is the language the web renders in, so scraping with it has a logic Python can’t match: when you drive a headless browser, the page’s own scripts and your scraper speak the same language. This guide is the full path for Node.js. Which tool does what, a fetch-plus-Cheerio walkthrough you can paste and run, how to handle JavaScript-rendered pages with Playwright, how to paginate, and how to stay unblocked at scale. I ran every snippet on Node v22.18.0 in June 2026, against books.toscrape.com and quotes.toscrape.com, two sandboxes built for practice, and pasted the real output.

Is Node.js good for web scraping?

Yes, Node.js is a strong choice for web scraping, and it has one structural advantage over every other language. JavaScript runs the browser, so when a page renders its content with client-side scripts and you reach for a headless browser, your scraper and the page run the same language end to end. There is no context switch between the runtime you scrape with and the runtime the page was written in.

Three things make Node pull its weight here:

Python has more scraping tutorials and a deeper bench of data libraries, which I cover in the Python guide. Node wins when the targets are JavaScript-heavy or your stack is already JavaScript.

What are the main JavaScript web scraping libraries?

Four tools cover the whole field, and the right one depends on whether the page needs JavaScript to render. Static HTML needs only a request and a parser. JavaScript-rendered pages need a real browser engine.

ToolRoleRuns page JavaScriptBest for
fetch (built-in)HTTP client, no parsingNoAny GET/POST, JSON APIs
AxiosHTTP client with extrasNoInterceptors, retries, shared config
CheerioHTML parser, jQuery-style selectorsNoParsing static HTML
Playwright / PuppeteerDrives a real ChromiumYesJS pages, clicks, logins, scroll

A few notes from using all four:

How do you set up a Node.js scraper?

Make a project, then install only what the page needs. The HTTP client is built in, so a static scrape needs just a parser:

mkdir my-scraper && cd my-scraper
npm init -y
npm install cheerio

For JavaScript-rendered pages, add a browser. Playwright downloads its own matching Chromium, so there is no separate driver to manage:

npm install playwright
npx playwright install chromium

Use ES modules so top-level await works (the snippets below rely on it). Either set "type": "module" in package.json or name your files .mjs. Here is the exact toolchain I tested on:

ComponentVersion
Node.jsv22.18.0
Cheerio1.2.0
Axios1.17.0
Playwright1.60.0
Chromium (via Playwright)148.0.7778.96

How do you scrape a website with Node.js step by step?

The loop is two steps: fetch the page into an HTML string, then pull fields with Cheerio selectors. This script fetches the first books.toscrape.com catalogue page and prints the title and price for the first three books:

import * as cheerio from "cheerio";

const url = "https://books.toscrape.com/catalogue/page-1.html";

const res = await fetch(url, {
  headers: { "User-Agent": "Mozilla/5.0 (compatible; BookScraper/1.0)" },
});
console.log("status:", res.status);

const html = await res.text();
const $ = cheerio.load(html);

const books = $("article.product_pod");
console.log("books on page:", books.length);

books.slice(0, 3).each((i, el) => {
  const title = $(el).find("h3 a").attr("title");
  const price = $(el).find(".price_color").text();
  console.log(`${price}  ${title}`);
});

Running it:

status: 200
books on page: 20
£51.77  A Light in the Attic
£53.74  Tipping the Velvet
£50.10  Soumission

Each part maps to one job:

This is the pattern I ship for any static page. The selectors are the only part that changes per site.

Follow the “next” link until it disappears. Most paginated lists put a next-page anchor at the bottom of each page, so you scrape a page, read that link, and repeat until there is no next link. This crawler walks the entire books.toscrape.com catalogue:

import * as cheerio from "cheerio";

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const base = "https://books.toscrape.com/catalogue/";

let next = "page-1.html";
let pages = 0;
let total = 0;

while (next) {
  const res = await fetch(base + next, {
    headers: { "User-Agent": "Mozilla/5.0 (compatible; CatalogueCrawler/1.0)" },
  });
  const $ = cheerio.load(await res.text());

  total += $("article.product_pod").length;
  pages += 1;

  const href = $("li.next a").attr("href");
  next = href || null;

  await sleep(300); // be polite
}

console.log("pages crawled:", pages);
console.log("total books: ", total);

Running it:

pages crawled: 50
total books:  1000

Two details make this robust on a real target. The li.next a lookup returns undefined on the last page, which is the natural stop signal for the while loop. And sleep(300) paces the crawl so you are not hammering the server; on a production job, also wrap the fetch in a try/catch so one failed page does not kill the run, and read the site’s robots.txt and terms first. The same “find the next anchor, repeat” loop handles any next-button pagination.

How do you scrape JavaScript-rendered pages in Node.js?

Use a real browser, because Cheerio cannot run JavaScript. When a page loads its data with client-side scripts after the initial response, that data is absent from the HTML fetch downloads, so your Cheerio selectors find nothing. Playwright launches a real Chromium, runs those scripts, and hands you the finished DOM.

Here is the proof. quotes.toscrape.com has a JavaScript build at /js/ where the quotes are injected by script after load. I scraped it two ways in one file: once with fetch + Cheerio (no browser), once with Playwright (real browser):

import * as cheerio from "cheerio";
import { chromium } from "playwright";

const url = "https://quotes.toscrape.com/js/";

// 1) Plain fetch: JavaScript has not run
const html = await fetch(url, {
  headers: { "User-Agent": "Mozilla/5.0" },
}).then((r) => r.text());
const $ = cheerio.load(html);
console.log("fetch + cheerio: .quote in raw HTML:", $(".quote").length);

// 2) Playwright: the browser runs the JavaScript
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle" });
const count = await page.locator(".quote").count();
console.log("playwright: .quote in rendered DOM:", count);

const first = await page.locator(".quote .text").first().textContent();
console.log("first quote:", first);

await browser.close();

Running it:

fetch + cheerio: .quote in raw HTML: 0
playwright: .quote in rendered DOM: 10
first quote: “The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”

fetch sees nothing because the quotes do not exist until JavaScript runs. Playwright sees all 10 because Chromium executed that script and built the DOM. The key line is waitUntil: "networkidle", which holds until network activity settles so the injected content is present before you read it. When fetch returns a shell with no data, this is the gap a browser fills. For the browser-automation deep dive in Python, see the Selenium tutorial; the same idea applies in Node with Playwright.

How do you paginate with a headless browser?

Click the “Next” link and re-read the DOM each time. With a real browser you drive the page the way a user would: scrape, click next, wait for the new content, repeat. This Playwright script walks all 10 pages of the JavaScript quotes build and collects every quote:

import { chromium } from "playwright";

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("https://quotes.toscrape.com/js/", { waitUntil: "networkidle" });

const rows = [];
let pages = 0;

while (true) {
  await page.waitForSelector(".quote");
  const onPage = await page.locator(".quote").evaluateAll((nodes) =>
    nodes.map((n) => ({
      text: n.querySelector(".text").innerText,
      author: n.querySelector(".author").innerText,
    }))
  );
  rows.push(...onPage);
  pages += 1;

  const next = page.locator("li.next > a");
  if ((await next.count()) === 0) break;
  await next.click();
}

await browser.close();

console.log("pages crawled:", pages);
console.log("quotes collected:", rows.length);
console.log("unique authors:", new Set(rows.map((r) => r.author)).size);
console.log("last author:", rows[rows.length - 1].author);

Running it:

pages crawled: 10
quotes collected: 100
unique authors: 50
last author: George R.R. Martin

page.waitForSelector(".quote") is the habit that keeps browser scraping reliable: it pauses until the element exists rather than guessing with a fixed sleep. evaluateAll runs in the page context and extracts every quote in one round trip, which is faster than reading nodes one at a time. The empty li.next count on the last page ends the loop cleanly.

When should you use Axios instead of fetch?

Reach for Axios when you want batteries that fetch leaves out: automatic JSON parsing, interceptors, per-request timeouts, and retry logic configured once. For one-off GETs the built-in fetch is enough, but on a larger codebase a shared Axios client removes boilerplate. This version fetches the same page, builds an array with Cheerio’s .map(), and writes it to JSON:

import axios from "axios";
import * as cheerio from "cheerio";
import { writeFileSync } from "node:fs";

const { data, status } = await axios.get(
  "https://books.toscrape.com/catalogue/page-1.html",
  { headers: { "User-Agent": "Mozilla/5.0 (compatible; BookScraper/1.0)" } }
);
console.log("axios status:", status);

const $ = cheerio.load(data);
const books = $("article.product_pod")
  .map((i, el) => ({
    title: $(el).find("h3 a").attr("title"),
    price: $(el).find(".price_color").text(),
  }))
  .get();

writeFileSync("books.json", JSON.stringify(books, null, 2));
console.log("rows:", books.length);
console.log("first:", JSON.stringify(books[0]));

Running it:

axios status: 200
rows: 20
first: {"title":"A Light in the Attic","price":"£51.77"}

Two ergonomics show up here. Axios parses the response for you and puts the body on .data, so there is no await res.text() step. And Cheerio’s .map(...).get() turns matched elements straight into a plain array of objects, which JSON.stringify then writes to disk. Swapping writeFileSync for a CSV builder is a few more lines.

How do you scrape at scale without getting blocked?

Look like a real browser, slow down, and spread requests across IP addresses. A site flags scrapers by three signals: requests that arrive too fast, headers that look automated, and too much traffic from one IP. Each has a countermeasure that applies whether you use fetch, Axios, or Playwright.

Block signalWhat triggers itFix
Bad headersEmpty or default User-Agent, no browser headersSet a realistic User-Agent and add browser-like headers
Request rateDozens of requests per second from one clientAdd delays, respect 429 responses, back off
Single IPThousands of requests from one addressRotate IPs or proxies, or use a scraper API
Automation fingerprintHeadless browser leaks (navigator.webdriver, etc.)Mask flags, or hand rendering to a scraper API

These measures carry a small project a long way. The trouble starts at scale: rotating a large IP pool, solving CAPTCHAs, masking headless fingerprints, and handling per-site quirks becomes its own engineering job, and it pulls focus from the data you actually want. That is where a scraper API like ChocoData fits the category, with a universal endpoint plus 453 dedicated endpoints that return the rendered page so your Node code keeps working unchanged while the blocking is handled upstream. You point fetch at the API instead of the target, parse the response with Cheerio the same way, and skip the proxy and CAPTCHA plumbing. I cover that full tradeoff, and the legal side, in the web scraping guide and in how to scrape without getting blocked.

For the scraping itself, the stack in this guide will carry you far. Take the Cheerio crawler for static pages and the Playwright crawler for JavaScript pages, point them at a site you care about, adjust the selectors until the fields come out clean, and add the anti-block measures as the target pushes back. The quick test for which path you need stays the same: fetch a page and search the result for a value you can see in the browser. When it is missing, the page is JavaScript-rendered and you move to a browser.

FAQ

Is Node.js good for web scraping?

Yes, and it has a structural edge: JavaScript is the language of the browser, so when you drive Playwright or Puppeteer the page scripts and your scraper share one language. The async model handles many concurrent requests well, and Cheerio gives you the jQuery selector API for parsing. Python has more tutorials, but for JavaScript-heavy targets Node is a natural fit.

Do I still need Axios or node-fetch in 2026?

Not for basic requests. Node 18 (April 2022) shipped a global fetch built on the same API browsers use, so plain GETs need no dependency. Axios is still worth installing when you want interceptors, automatic JSON handling, retry logic, or one client config reused across a large codebase.

Should I use Puppeteer or Playwright?

Both drive a real Chromium and run page JavaScript. Playwright is cross-browser (Chromium, Firefox, WebKit) from one API and has auto-waiting baked in, which removes most flaky-wait code. Puppeteer is Chrome-focused and slightly lighter. For a new scraping project I lean Playwright; if you only target Chrome, Puppeteer is fine.

MR
Marcus Reed
I've built and run web scrapers for the better part of a decade. On this site I put scraper APIs and scraping tools through real jobs against real targets, then write up what actually holds up.