Tutorial8 min read

Web Scraping With JavaScript and Node.js

Short answer

Web scraping with JavaScript runs in Node.js: you download a page with the built-in fetch (or Axios), parse the HTML with Cheerio and pick fields with CSS selectors, and you switch to Puppeteer or Playwright only when the page builds its content in the browser. A static-page scraper is about 20 lines and needs one package, cheerio, on Node.js 22 or later. ProxyField's residential proxies plug into Node.js through a proxy agent, with 412M+ IPs in 195+ countries from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.

residential IPs
412M+
from, traffic never expires
$1.95/GB
Easy Trial
1 GB for $2.95
On this page 11 sections
  1. Can you scrape websites with JavaScript?
  2. Which JavaScript scraping library to use
  3. Step 1: check whether the page needs a browser
  4. Step 2: scrape a static site with fetch and Cheerio
  5. Step 3: crawl a whole website with JavaScript
  6. Step 4: scrape JavaScript-rendered pages with Puppeteer
  7. Web scraping with TypeScript
  8. Screen scraping in Node.js
  9. Benefits and challenges of scraping with Node.js
  10. Using proxies in Node.js scrapers
  11. Web Scraping With JavaScript and Node.js FAQ

Can you scrape websites with JavaScript?

Yes, in Node.js. JavaScript is a natural fit for scraping because it is the language of the web: the same CSS selectors you test in the browser's DevTools work in your scraper, JSON is native, and Node's asynchronous I/O fetches many pages at once without threads. Node.js also has the two most used browser automation libraries, Puppeteer and Playwright.

Scraping from a script in the browser itself (the DevTools console or a bookmarklet) works only for the page you are on: browsers block a page from reading other sites' responses unless those sites allow it (the same-origin policy and CORS, MDN: CORS). Node.js has no such limit, which is why scrapers run there.

Which JavaScript scraping library to use

LibraryWhat it doesRuns JavaScript on the pageUse it for
fetch (built into Node.js since version 18)Downloads pages and JSONNoMost requests; no install
AxiosHTTP client with interceptors, timeouts and retries via pluginsNoProjects that already use it
CheerioParses HTML and selects with a jQuery-like APINoExtracting fields from static HTML
jsdomA DOM implementation in Node.jsLimitedPages that need DOM APIs without a browser
PuppeteerDrives Chrome or FirefoxYesJavaScript-rendered pages, clicks, scrolls
PlaywrightDrives Chromium, Firefox, WebKitYesThe same, with auto-waiting and contexts
CrawleeCrawling framework with queues, retries, proxy rotation; HTTP and browser crawlersOptionalLarge crawls mixing both kinds of pages

A wider list across languages is in web scraping tools compared.

Step 1: check whether the page needs a browser

Open the page, press Ctrl+U (Cmd+Option+U on a Mac) to view the source the server sent, and search for a value you want, such as a price. If it is in the source, the page is static for your purposes: use fetch and Cheerio. If the source is an almost empty <div id="root"> and the data appears only in the rendered page, it is built by JavaScript (React, Vue or AJAX calls): look for its JSON request first (DevTools, Network tab, Fetch/XHR), and use Puppeteer or Playwright only if there is no clean request to call.

Step 2: scrape a static site with fetch and Cheerio

The script below collects every book's title, price and stock status from a practice catalog, follows the "next" link through all pages, waits a second between pages, and saves JSON.

Shell
mkdir books-scraper && cd books-scraper
npm init -y && npm pkg set type=module
npm i cheerio
JavaScript
// scrape.mjs  ->  node scrape.mjs
import * as cheerio from 'cheerio';
import { writeFile } from 'node:fs/promises';

const books = [];
let url = 'https://books.toscrape.com/catalogue/page-1.html';

while (url) {
  const res = await fetch(url, { headers: { 'user-agent': 'Mozilla/5.0 (compatible; books-demo/1.0)' } });
  if (!res.ok) throw new Error(`HTTP ${res.status} on ${url}`);
  const $ = cheerio.load(await res.text());

  $('article.product_pod').each((_, el) => {
    books.push({
      title: $(el).find('h3 a').attr('title'),
      price: $(el).find('.price_color').text().trim(),
      inStock: $(el).find('.instock').length > 0,
    });
  });

  const next = $('li.next a').attr('href');
  url = next ? new URL(next, url).href : null;     // resolve the relative link
  await new Promise(r => setTimeout(r, 1000));     // one request per second
}

await writeFile('books.json', JSON.stringify(books, null, 2));
console.log(`Saved ${books.length} books`);

How it works: fetch returns the HTML, cheerio.load() turns it into a queryable document, $(selector) finds elements as in jQuery, and new URL(next, url) turns the relative "next" link into an absolute address.

Tables and lists

HTML tables map straight to rows:

JavaScript
const rows = $('table tr').map((_, tr) =>
  [$(tr).find('th, td').map((_, c) => $(c).text().trim()).get()]).get();

Each inner array is one row; write it to CSV by joining cells with commas after escaping quotes, or use a CSV library.

Saving the data

JSON.stringify(data, null, 2) with writeFile is enough for most jobs. For large crawls, write one JSON object per line (JSON Lines) as you go, so a crash does not lose what you already scraped:

JavaScript
import { appendFile } from 'node:fs/promises';
await appendFile('books.jsonl', JSON.stringify(book) + '\n');

Step 3: crawl a whole website with JavaScript

A crawler starts from one URL, collects the links on each page, and visits the ones on the same site it has not seen. A queue, a Set of visited URLs and a page limit are all it needs:

JavaScript
import * as cheerio from 'cheerio';

const start = new URL('https://books.toscrape.com/');
const queue = [start.href];
const seen = new Set(queue);
const MAX_PAGES = 200;
let visited = 0;

while (queue.length && visited < MAX_PAGES) {
  const url = queue.shift();
  visited++;
  const res = await fetch(url);
  if (!res.ok || !res.headers.get('content-type')?.includes('text/html')) continue;
  const $ = cheerio.load(await res.text());
  console.log(url, '-', $('title').text().trim());

  $('a[href]').each((_, a) => {
    const link = new URL($(a).attr('href'), url);
    link.hash = '';
    if (link.host === start.host && !seen.has(link.href)) {
      seen.add(link.href);
      queue.push(link.href);
    }
  });
  await new Promise(r => setTimeout(r, 500));
}

Before you crawl a site that is not yours, read its robots.txt and terms, keep the request rate low, and stop at a page limit. For crawls of thousands of pages, a framework such as Crawlee adds persistent queues, retries, concurrency limits and proxy rotation. The general method is in how to crawl a website.

Step 4: scrape JavaScript-rendered pages with Puppeteer

When the data is not in the page source, run the page in a browser. Puppeteer loads it, runs its scripts, and lets you read the rendered DOM:

JavaScript
// npm i puppeteer
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://quotes.toscrape.com/js/', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.quote');
const quotes = await page.$$eval('.quote .text', els => els.map(e => e.textContent));
console.log(quotes);
await browser.close();

Waits, infinite scroll, screenshots and performance are covered in web scraping with Puppeteer; the Playwright version is in web scraping with Playwright. For pages built with React or loaded by AJAX, check the Network tab first: the JSON the page requests is usually easier to scrape than the page.

JavaScript
const data = await (await fetch('https://example.com/api/products?page=2')).json();

Web scraping with TypeScript

Everything above works in TypeScript: Cheerio, Puppeteer, Playwright and Crawlee ship their own type definitions. Types help most in scraping where they define the shape of a scraped record, so a selector that returns undefined fails at the type check or the validation step instead of in your database. Run TypeScript files directly with npx tsx scrape.ts, or on Node.js versions that strip types natively.

Screen scraping in Node.js

"Screen scraping" is the older term for reading data from what a program displays; on the web it means the same as browser-based scraping. In Node.js, that is Puppeteer or Playwright reading the rendered page, or taking screenshots with page.screenshot() when you need the visual result. Reading text from images (OCR) is a different job that needs an OCR library.

Benefits and challenges of scraping with Node.js

Benefits:

  • One language for the browser, the scraper and often the backend.
  • Asynchronous I/O fetches many pages concurrently with little code.
  • The DevTools selectors you test by hand work unchanged in Cheerio and Puppeteer.
  • First-class browser automation (Puppeteer, Playwright) and crawling frameworks (Crawlee).

Challenges and fixes:

ChallengeFix
Content loaded by JavaScriptCall the page's JSON endpoint, or use Puppeteer or Playwright
Rate limits, 403 and 429 responsesSlow down, retry with backoff, rotate IPs; see how anti-bot systems detect scrapers
Layout changes break selectorsPrefer IDs, data-* attributes and embedded JSON; validate each record
Memory with many browser pagesCap concurrency, reuse one browser, close pages
Unhandled promise rejections stop the runWrap each page in try/catch, log the URL and continue

Using proxies in Node.js scrapers

When a scraper sends thousands of requests from one IP, sites start to slow it down or return errors. A rotating residential proxy gives each request a new home IP. Use undici's fetch with its ProxyAgent as the dispatcher; importing both from the same package keeps their versions matched:

JavaScript
// npm i undici
import { fetch, ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent('http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000');
const res = await fetch('https://api.ipify.org?format=json', { dispatcher });
console.log(await res.json());   // the exit IP

<login>-country-us-city-new_york asks for an IP in New York, United States. Region, session and session time go in the same username: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default), as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional. Axios, node-fetch, SOCKS5 and rotation are in the Node.js proxy setup guide. ProxyField residential proxies cost $2.95/GB for 1 GB down to $1.95/GB from 1,000 GB, with no subscription; the traffic never expires, and all purchases are final (residential and mobile proxy pricing). Collect only public data and check each site's terms; see is web scraping legal.

Web Scraping With JavaScript and Node.js FAQ

Is JavaScript good for web scraping?

ProxyField's residential proxies (412M+ residential IPs, from $1.95/GB) plug into Node.js through a proxy agent, and JavaScript is good for web scraping there: fetch and Cheerio handle static pages, Puppeteer or Playwright handle JavaScript-rendered ones. Scripts in the browser console are limited to the page you are on.

How do I scrape a website with Node.js?

Install Cheerio, download the page with the built-in fetch, load the HTML with cheerio.load(), select the fields with CSS selectors and save them as JSON. If the data is not in the page source, use Puppeteer or Playwright to render the page first.

How do I scrape static websites with JavaScript?

Use fetch and Cheerio: no browser is needed when the data is in the HTML the server sends. Follow the "next" links with new URL(href, pageUrl) and pause between requests.

What are the benefits of web scraping with Node.js?

One language for the browser and the scraper, asynchronous I/O that fetches many pages at once, and CSS selectors you can test in DevTools. Node.js also handles JSON natively and has the main browser automation libraries, Puppeteer and Playwright.

Is Axios better than fetch for web scraping in Node.js?

Not for most scrapers: the built-in fetch needs no install and handles pages and JSON. Axios adds interceptors, automatic JSON parsing and a large plugin set, which helps if your project already uses it; for proxies, both work through a proxy agent.

How do I crawl a website with Node.js?

Keep a queue of URLs and a Set of seen ones, fetch each page, extract its links with Cheerio, resolve them with new URL(), and add the same-site links you have not seen. Cap the page count and pause between requests; Crawlee does the same with persistent queues and retries.

How can I tell if a website is using JavaScript to load its content?

View the page source (Ctrl+U) and search for the data you see on screen. If it is missing from the source, or the body is a nearly empty root element, the content is built by JavaScript; the DevTools Network tab then shows the JSON requests that bring the data.

How can I scrape websites that use AJAX or React with Node.js?

First find the JSON request the page makes in the Network tab and call it with fetch. If the data only exists in the rendered page, load it with Puppeteer or Playwright and wait for the element that holds it.

How can I scrape data from tables and lists using JavaScript?

With Cheerio, loop over table tr and map each row's th, td cells to their text; for lists, map li or the repeated card element to an object per item. Write the rows to CSV or JSON.

How can I save collected data as a JSON file when web scraping with Node.js?

Collect the records in an array and write JSON.stringify(records, null, 2) with writeFile from node:fs/promises. For long runs, append one JSON object per line as you go, so a crash keeps what you already have.

Show 2 more questionsShow fewer questions

Is TypeScript good for web scraping?

Yes: Cheerio, Puppeteer, Playwright and Crawlee include type definitions, and a typed record catches missing fields early. Run it with npx tsx scrape.ts or compile it to JavaScript.

What are some challenges of web scraping with JavaScript?

Pages that render in the browser, rate limits and blocks, selectors that break when layouts change, memory use with many browser pages, and unhandled promise rejections that stop a run. Calling JSON endpoints, pacing requests, validating records and catching errors per page solve most of them.

Easy Trial: 1 GB for $2.95.

Try for the price of 1 GB, no subscription, traffic never expires.