Guides
Cheerio web scraping in Node.js: fast HTML parsing without a browser
Use Cheerio with Node.js to parse server-rendered HTML, extract records, follow pagination, and know exactly when Cheerio is the wrong tool.
Tested with Node.js 24.13.0, Cheerio 1.2.0 on 2 November 2026. Library versions and dates are part of the evidence, not decoration.
Cheerio is a fast HTML parser for Node.js with a jQuery-like API. It does not run JavaScript and does not fetch pages; it takes an HTML string and gives you $, so you can select elements and extract text. It is the right default for server-rendered pages.
If the data is not in the HTML, Cheerio will return an empty selection. That is not a selector bug; it is the browser boundary.
Install
mkdir cheerio-scrape && cd cheerio-scrape
npm init -y
npm pkg set type=module
npm install cheerio
Tested with Node.js 24.13.0 and Cheerio 1.2.0 on 2 November 2026.
Fetch and parse
import { load } from 'cheerio'
const response = await fetch('http://127.0.0.1:8765/items?page=1', {
headers: {
'User-Agent': 'CrawlerBros-Example/1.0 (+https://www.crawlerbros.com/about)',
Accept: 'text/html',
},
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const html = await response.text()
const $ = load(html)
const rows = $('ul.results li.item').map((_, element) => ({
sku: $(element).attr('data-sku'),
name: $(element).find('h3').text().trim(),
price: $(element).find('.price').text().trim(),
})).get()
console.log(JSON.stringify(rows, null, 2))
Output against the fixture:
[
{
"sku": "A-1",
"name": "Mechanical keyboard",
"price": "$89.00"
},
{
"sku": "B-2",
"name": "USB-C hub",
"price": "$39.50"
}
]
$(element) scopes the search to one card. That prevents the “titles and prices drifted out of sync” bug you get from two independent $('h3') and $('.price') lists.
Pagination
let url = 'http://127.0.0.1:8765/items?page=1'
const records = []
const seen = new Set()
while (url && !seen.has(url)) {
seen.add(url)
const response = await fetch(url, {
headers: { 'User-Agent': 'CrawlerBros-Example/1.0 (+https://www.crawlerbros.com/about)' },
})
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`)
const $ = load(await response.text())
$('ul.results li.item').each((_, element) => {
records.push({
sku: $(element).attr('data-sku'),
name: $(element).find('h3').text().trim(),
price: $(element).find('.price').text().trim(),
})
})
url = $('a[rel="next"]').attr('href')
? new URL($('a[rel="next"]').attr('href'), url).href
: null
}
console.log(records.length, records.map(record => record.sku))
Tested output:
5 [ 'A-1', 'B-2', 'C-3', 'D-4', 'E-5' ]
The seen set stops a loop when a next link points back to the current page. A missing next link is the normal end.
Cheerio selectors
Cheerio uses css-select, which supports most CSS selectors:
$('article.card h2') // descendants
$('article.card > h2') // direct children
$('a[href^="/docs"]') // attribute prefix
$('tr:nth-child(2) td') // positional
$('p').filter((_, element) => $(element).text().includes('Price')) // text filter
Cheerio does not support :contains() or :has() in the same way jQuery does. For text matching, use .filter() with .text(), or select a stable parent and walk.
When Cheerio is the wrong tool
- Client-rendered pages. If the data appears only after JavaScript,
load(html)sees the shell. Find the JSON endpoint or use Playwright/Puppeteer. - Pages that need clicks, scrolls, or a session. Cheerio has no browser.
- A single request that returns JSON. Skip the parser and use
response.json(). - A site that requires auth you do not have permission to use. A parser does not change the permission.
Cheerio vs BeautifulSoup
| Criterion | Cheerio | BeautifulSoup |
|---|---|---|
| Language | Node.js | Python |
| Speed | Fast | Slower (pure Python backend) |
| CSS selectors | Yes | Yes |
| XPath | No | No (use lxml) |
| Tree editing | Limited | Yes |
| Best for | Node HTTP pipelines | Python scripts and forgiving parsing |
If you are in Node and the page is server-rendered, Cheerio is the default. If you need XPath or a browser, choose a different tool.