# Quick Guide to Building Your First NodeJS Web Scraper
> Source: https://scrape.do/blog/build-web-scraper-nodejs/
Published: 2025-05-15 · Updated: 2025-05-16 · Authors: Mert Bekci · Categories: Scraping Basics
Collecting data by hand feels like watching paint dry; at scale it becomes a non-starter.
With **Node.js** you can turn that slog into an automated pipeline that fetches pages, extracts the nuggets you need, and saves them in neat, machine-readable files.
Market research, competitor monitoring, custom dashboards; **they all start with reliable scraping.**
***Here's what you'll learn in the next few minutes:***
* Setting up Node.js and npm (or nvm if you want an easy version manager)
* Installing essential libraries with `npm`
* Making your first HTTP request and checking the response code
* Parsing the returned HTML using Cheerio’s jQuery-style selectors
* Writing scraped data to JSON and CSV for easy analysis
* Previewing headless browsers and anti-bot tactics so JavaScript-heavy sites and CAPTCHAs do not trip you up
By the end you will have a working scraper in a single JavaScript file; the foundation for bigger automation projects.
## Preparing Your Node.js Environment
Before you write a single line of scraping code, make sure your toolkit is in place; once these short steps are done, the rest of the guide will run smoothly.
### Install Node.js (and npm)
* Head to the official Node.js site and download the current LTS installer (Windows and macOS) or use your package manager on Linux.
* The installer bundles **npm** (Node Package Manager), so you get both with one click.
### Optional but recommended — use a version manager
* **nvm** on macOS /Linux or **nvm-windows** on Windows lets you switch Node versions effortlessly; this prevents “works-on-my-machine” problems when different projects need different runtimes.
* After installing nvm, one command installs the latest LTS release:
```bash
nvm install --lts
nvm use --lts
```
### Confirm everything is ready
Open a terminal and run:
```bash
node -v
npm -v
```
You should see two version numbers (for Node.js and npm). If either command fails, double-check your installation or PATH.
### Create a workspace and initialize the project
```bash
mkdir node-web-scraper
cd node-web-scraper
npm init -y # generates a minimal package.json
```
A dedicated folder keeps dependencies and output files organized; the `-y` flag accepts sensible defaults.
### Pick an editor and open the folder
Most Node developers start with **VS Code** (lightweight and feature-rich); others prefer **WebStorm** (full IDE) or **Sublime Text** (fast and minimal). Open the `node-web-scraper` folder in your editor so IntelliSense and linting work immediately.
#### Run a quick sanity check
1. Inside the project, create **scraper.js** with:
```js
console.log('Scraping environment is ready!');
```
2. In the terminal, execute:
```bash
node scraper.js
```
You should see:
```css
Scraping environment is ready!
```
If that message appears, Node.js is installed; npm is configured; your editor is set; and the project is ready for the next step; **sending your first HTTP request.**
## Sending Your First Request
Before we parse or export anything we need proof that our program can reach a web page and receive a valid response. In Node.js the easiest way to do that is by using **axios**, a promise-based HTTP client that works both in Node and in the browser.
### Install axios
```bash
npm install axios
```
When the command finishes you can confirm it worked:
```bash
npm list axios # shows the version in the dependency tree
```
### A quick library sanity check
Create a file named **check-axios.js**.
```js
// check-axios.js
const axiosPkg = require('axios/package.json');
console.log('Axios version:', axiosPkg.version);
```
Run it with:
```bash
node check-axios.js
```
If a version string appears, axios is ready to use; you can delete the file once you are satisfied.
### Fetch the HTTP status code
Now let us write a short script that requests a practice page and prints its status code. Place the following in **status-test.js**.
```js
const axios = require('axios');
async function testStatus() {
try {
const url = 'https://www.scrapethissite.com/pages/simple/';
const response = await axios.get(url);
console.log('Status Code:', response.status);
} catch (err) {
console.error('Request failed:', err.message);
}
}
testStatus();
```
Run:
```bash
node status-test.js
```
A healthy request prints:
```css
Status Code: 200
```
*200* means the page loaded successfully.
If you receive *403* the site has blocked you; *404* indicates the page does not exist.
### Preview a slice of the HTML
Seeing the raw markup helps when you debug parsing issues.
Add one extra line to the previous script:
```js
console.log('HTML preview:', response.data.slice(0, 500));
```
Run it again; you will see the opening of the document including the `
` and the first heading. This confirms that axios delivered the full HTML and that your program can move on to parsing with Cheerio in the next section.
## Parsing and Extracting Data with Cheerio
Fetching the page confirms that our request works; the next goal is to navigate the HTML and pull out the exact elements we care about.
In Node.js the go-to library for this job is **Cheerio**; it gives you a jQuery-style API for querying a DOM that lives entirely in memory (no browser needed).
### Install Cheerio
```bash
npm install cheerio
```
Don't forget to verify the install:
```bash
npm list cheerio
```
### Load HTML and read the `` heading
Create **parse-h1.js**.
```js
const axios = require('axios');
const cheerio = require('cheerio');
async function grabHeading() {
const url = 'https://www.scrapethissite.com/pages/simple/';
const { data: html } = await axios.get(url);
const $ = cheerio.load(html); // turn raw HTML into a queryable object
const h1Text = $('h1').first().text().trim();
console.log('Page title:', h1Text);
}
grabHeading();
```
Run it:
```bash
node parse-h1.js
```
Expected output:
```css
Page title: Countries of the World: A Simple Example
```
### Extract every country name and capital
Each country block on the demo page looks like this (simplified):
```html
Andorra
Capital: Andorra la Vella
```
Knowing that structure we can loop through all `div.country` nodes and grab the nested elements.
Create **scrape-countries.js**.
```js
const axios = require('axios');
const cheerio = require('cheerio');
async function scrapeCountries() {
const url = 'https://www.scrapethissite.com/pages/simple/';
const { data: html } = await axios.get(url);
const $ = cheerio.load(html);
const results = [];
$('div.country').each((_, el) => {
const name = $(el).find('h3.country-name').text().trim();
const capital = $(el).find('span.country-capital').text().trim();
results.push({ country: name, capital });
});
console.log(`Total countries found: ${results.length}`);
console.log(results.slice(0, 5)); // preview the first five objects
}
scrapeCountries();
```
Sample output:
```json
Total countries found: 250
[
{ country: 'Andorra', capital: 'Andorra la Vella' },
{ country: 'United Arab Emirates', capital: 'Abu Dhabi' },
{ country: 'Afghanistan', capital: 'Kabul' },
{ country: 'Antigua and Barbuda', capital: "St. John's" },
]
```
### If nothing shows up
Sometimes selectors fail because a website changes its HTML. Right-click the page, choose **Inspect** (or **View Source**) and confirm the tags and classes.
💡 For example, if the heading changed to ``, you would adjust:
```js
const name = $(el).find('h2.name').text().trim();
```
The skill of reading page structure is fundamental; with a reliable set of selectors you can scrape almost any static site.
## Exporting Your Scraped Data
Printing objects to the console is fine for a sanity-check; real projects need the data saved in a format other tools can read. The two staples are **JSON** (great for APIs and JavaScript apps) and **CSV** (ideal for spreadsheets and databases).
### Save as JSON
Node already knows how to turn objects into JSON strings; you only need the built-in **fs** module.
Create **save-json.js**.
```js
const fs = require('fs');
const axios = require('axios');
const cheerio = require('cheerio');
async function run() {
const url = 'https://www.scrapethissite.com/pages/simple/';
const { data: html } = await axios.get(url);
const $ = cheerio.load(html);
const countries = [];
$('div.country').each((_, el) => {
const name = $(el).find('h3.country-name').text().trim();
const capital = $(el).find('span.country-capital').text().trim();
countries.push({ country: name, capital });
});
fs.writeFileSync('countries.json', JSON.stringify(countries, null, 2));
console.log('✔ Data written to countries.json');
}
run();
```
Run the script:
```bash
node save-json.js
```
Open **countries.json** and you’ll see pretty-printed data:
```json
[
{
"country": "Andorra",
"capital": "Andorra la Vella"
},
{
"country": "United Arab Emirates",
"capital": "Abu Dhabi"
}
// …
]
```
### Save as CSV
CSV is just plain text with commas; you could build the string yourself, but a helper library avoids edge cases (commas inside names, Unicode, line endings). A tiny, no-dependency package called **csv-writer** does the job.
```bash
npm install csv-writer
```
Create **save-csv.js**.
```js
const fs = require('fs');
const axios = require('axios');
const cheerio = require('cheerio');
const { createObjectCsvWriter } = require('csv-writer');
async function run() {
const url = 'https://www.scrapethissite.com/pages/simple/';
const { data: html } = await axios.get(url);
const $ = cheerio.load(html);
const rows = [];
$('div.country').each((_, el) => {
rows.push({
Country: $(el).find('h3.country-name').text().trim(),
Capital: $(el).find('span.country-capital').text().trim()
});
});
const csvWriter = createObjectCsvWriter({
path : 'countries.csv',
header : [
{ id: 'Country', title: 'Country' },
{ id: 'Capital', title: 'Capital' }
],
fieldDelimiter: ',', // default
alwaysQuote : true // keeps commas in values safe
});
await csvWriter.writeRecords(rows);
console.log('✔ Data written to countries.csv');
}
run();
```
Run:
```bash
node save-csv.js
```
Open **countries.csv** in Excel, Google Sheets, or a text editor:
```css
"Country","Capital"
"Andorra","Andorra la Vella"
"United Arab Emirates","Abu Dhabi"
"Afghanistan","Kabul"
```
So your two choices are:
* **JSON**: perfect for programmatic consumption or RESTful APIs.
* **CSV**: quick to inspect in spreadsheets, easy to import into SQL.
With reliable export in place, the next challenge is scraping sites that hide content behind JavaScript. We’ll tackle that with a headless browser in the upcoming section.
## Handling JavaScript-Heavy Pages with a Headless Browser
A regular `axios` request is enough for pages that return all their data inside the initial HTML; many modern sites do the heavy lifting in JavaScript instead. When that happens you must run a real browser engine (just without the visible window) so scripts execute and dynamic elements appear.
### Popular headless options in the Node.js ecosystem
* [Puppeteer](https://scrape.do/blog/puppeteer-web-scraping/) — maintained by the Chrome team; it gives a high-level API that controls Chromium or Firefox programmatically and launches in headless mode by default
* [Playwright](https://scrape.do/blog/web-scraping-with-playwright/) — similar API but works with Chromium, Firefox, and WebKit from a single codebase; perfect when you need cross-browser coverage or extra speed.
* [Selenium](https://scrape.do/blog/selenium-web-scraping/) — language-agnostic classic; still useful for complex test automation but heavier than the two libraries above
We will use **Puppeteer**; the code style stays almost identical if you swap in Playwright later.
### Install Puppeteer and pull a page
```bash
npm install puppeteer # downloads a bundled Chromium
```
Create **browser-check.js**.
```js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch(); // headless by default
const page = await browser.newPage();
await page.goto('https://www.scrapethissite.com/pages/simple/', { waitUntil: 'networkidle0' });
const heading = await page.$eval('h1', el => el.textContent.trim());
console.log('Page title:', heading); // Countries of the World: A Simple Example
await browser.close();
})();
```
💡 `waitUntil: 'networkidle0'` pauses until the browser sees no new network requests for a short period; in practice that means the page and its JavaScript are done loading. Then we run a tiny DOM query inside the browser’s context (`page.$eval`) to extract the `` text.
### Scraping content that loads **after** a click
The Oscars demo page hides the movie list until you select a year; **axios would never see those rows.**
Using Puppeteer we can click the year, wait for the table, and collect the titles.
```js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const url = 'https://www.scrapethissite.com/pages/ajax-javascript/';
await page.goto(url, { waitUntil: 'domcontentloaded' });
// trigger the JavaScript that fetches 2015 winners
await page.click('a[href="#2015"]');
// wait until at least one table row appears
await page.waitForSelector('table.table tbody tr');
// grab the text of every film title cell
const titles = await page.$$eval('td.title', cells =>
cells.map(c => c.textContent.trim())
);
console.log('2015 Oscar winners:', titles.slice(0, 5));
await browser.close();
})();
```
You should see an array beginning with `"Spotlight"`, `"Mad Max: Fury Road"`, and so on—data that axios alone would have missed.
### When to reach for a headless browser
* The page is blank or missing key elements in the axios/cheerio output.
* You must interact (scroll, click, log in, type a search term).
* Anti-bot checks look for real browser fingerprints; many simple blocks vanish when Chromium is driving the request.
Headless browsers are slower than plain HTTP calls; **use them only when necessary** for better scaling and resource management.