~ / guides / Web Scraping with Cheerio: Complete Tutorial

Web Scraping with Cheerio: Complete Tutorial

MR
Marcus Reed
Founder & lead tester · about the author
the short version
  • Cheerio is a Node.js library that parses HTML and lets you query it with jQuery-style selectors like $('div.quote').
  • Pair it with the built-in fetch to download pages, and you can scrape a static site in about 15 lines.
  • I scraped all 100 quotes (50 authors) across 10 pages of quotes.toscrape.com with the code below to confirm every snippet runs.
  • Cheerio parses HTML only. It does not run JavaScript, so for pages that build content in the browser you add a headless browser or a scraper API.

Cheerio is the library I reach for first when I scrape a static site in Node.js. It parses HTML and hands you a jQuery-style API, so if you have ever written $('.price') in a browser console you already know most of it. This tutorial covers the whole loop: install it, fetch a page, select elements, walk pagination, and handle the JavaScript case. Every snippet here ran against quotes.toscrape.com, a sandbox built for practice, in June 2026 on Cheerio 1.2.0 and Node v22.18.0.

What is Cheerio?

Cheerio is a Node.js library that parses HTML and XML into a tree you query with CSS selectors. Its own docs describe it as “the fast, flexible & elegant library for parsing and manipulating HTML and XML.” You give it a string of markup, call load(), and get back a $ function that selects, traverses, and reads elements exactly like jQuery on the server.

It does one job and stays out of everything else. Per the documentation: “Cheerio is not a web browser. Cheerio parses markup and provides an API for traversing/manipulating the resulting data structure. It does not interpret the result as a web browser does. Specifically, it does not produce a visual rendering, apply CSS, load external resources, or execute JavaScript.” That means Cheerio does not fetch pages either, so you pair it with a request method.

The standard stack is three pieces:

PieceJob
fetch (built into Node 18+)Downloads the page over HTTP
CheerioParses the HTML and finds elements
css-selectThe selector engine Cheerio uses under the hood

How do you install Cheerio?

Install it from npm with a single command inside a Node project:

npm install cheerio

You do not need a separate request library. Node 18 and later ship a global fetch, and I ran everything below on Node v22.18.0, so the only dependency is Cheerio itself.

Import it at the top of your file. Cheerio 1.x ships as an ES module, so use a named import in a .mjs file or a project with "type": "module":

import * as cheerio from "cheerio";

If your project is CommonJS, const cheerio = require("cheerio"); works the same way.

How does Cheerio parse an HTML string?

Call cheerio.load() on any markup string and it returns the $ query function. This short script loads a snippet and reads it three ways:

import * as cheerio from "cheerio";

const $ = cheerio.load("<ul id='fruit'><li class='a'>Apple</li><li>Pear</li></ul>");

console.log("text of first li:", $("li").first().text());
console.log("class of first li:", $("li.a").attr("class"));
console.log("count of li:", $("#fruit li").length);

I ran it. Real output:

text of first li: Apple
class of first li: a
count of li: 2

Three methods cover most of what you will do: .text() reads inner text, .attr() reads an attribute, and .length counts matches. Selectors are standard CSS, so #fruit li, li.a, and div.quote > span all work the way they do in a stylesheet. For the difference between the two selector families, see my guide on CSS selectors and XPath.

How do you scrape a live page with Cheerio?

Fetch the page, pass the HTML to load(), then loop over the elements you want. This script pulls the text, author, and tags for every quote on the first page of quotes.toscrape.com:

import * as cheerio from "cheerio";

const url = "https://quotes.toscrape.com/";
const res = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0" } });
const html = await res.text();

const $ = cheerio.load(html);

const quotes = [];
$("div.quote").each((i, el) => {
  quotes.push({
    text: $(el).find("span.text").text(),
    author: $(el).find("small.author").text(),
    tags: $(el).find("div.tags a.tag").map((i, t) => $(t).text()).get(),
  });
});

console.log("found", quotes.length, "quotes on page 1");
console.log(JSON.stringify(quotes[0], null, 2));

I ran it. Real output:

found 10 quotes on page 1
{
  "text": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”",
  "author": "Albert Einstein",
  "tags": [
    "change",
    "deep-thoughts",
    "thinking",
    "world"
  ]
}

Two patterns do the heavy lifting. .each() iterates a selection and gives you each element, which you re-wrap with $(el) to scope further queries with .find(). .map().get() turns a selection into a plain array, which is how the tag list becomes a JavaScript array instead of a Cheerio object.

How do you handle pagination in Cheerio?

Read the “next page” link from each page and follow it until the link disappears. quotes.toscrape.com puts that link in an li.next a element, so the loop stops on the last page automatically when the selector returns nothing:

import * as cheerio from "cheerio";

const base = "https://quotes.toscrape.com";
let next = "/";
const all = [];

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

  $("div.quote").each((i, el) => {
    all.push({
      author: $(el).find("small.author").text(),
      text: $(el).find("span.text").text(),
    });
  });

  // .next li holds the link to the following page, if any
  const link = $("li.next a").attr("href");
  next = link || null;
}

console.log("total quotes:", all.length);
console.log("distinct authors:", new Set(all.map((q) => q.author)).size);

I ran it across the full site. Real output:

total quotes: 100
distinct authors: 50

The whole crawl, 10 pages, took a couple of seconds because there is no browser to start. When $("li.next a").attr("href") returns undefined on the final page, next becomes null and the while loop ends. Add a small delay between requests on real sites to stay polite. For the broader picture of building crawlers, see the web scraping pillar guide.

What happens when a page renders with JavaScript?

Cheerio sees the HTML as delivered, so content a page builds in the browser is invisible to it. quotes.toscrape.com has a /js/ variant that injects its quotes with JavaScript after the page loads. This script checks what Cheerio finds there:

import * as cheerio from "cheerio";

// This page builds its quote list in the browser with JavaScript.
const res = await fetch("https://quotes.toscrape.com/js/", {
  headers: { "User-Agent": "Mozilla/5.0" },
});
const $ = cheerio.load(await res.text());

console.log("quotes Cheerio sees:", $("div.quote").length);

I ran it. Real output:

quotes Cheerio sees: 0

Zero, against the 10 the static page returns. The quote data is sitting in a <script> tag as a JavaScript array, waiting for the browser to render it into the DOM. Cheerio never runs that script.

You have two ways forward when this happens:

SituationApproach
Data hides in a <script> tag (JSON or a JS variable)Extract the script text with Cheerio, then JSON.parse it
Page needs a real browser to assemble the DOMRender with Playwright or Puppeteer, then pass the rendered HTML to Cheerio

The first case is common and fast: many sites embed the same data you want as JSON inside a <script type="application/ld+json"> block, and Cheerio reads that text fine. Reach for a browser only when the DOM truly depends on execution.

How do you scrape at scale or get past blocks?

Cheerio parses HTML, and that is the limit of what it can do about blocking. It does not rotate IPs, solve CAPTCHAs, or render JavaScript, so the moment a target starts returning 403s or challenge pages your fetch call gets the block instead of the data, and Cheerio dutifully parses the error page. The common defenses are covered in my guide on scraping without getting blocked.

When a site fights back hard, the practical move is to keep Cheerio for parsing and put a scraper API in front of the request. A service like ChocoData handles proxy rotation, browser rendering, and retries behind one endpoint, so you fetch through its API and still pass the returned HTML to cheerio.load(). Its universal endpoint covers arbitrary URLs, and it adds 453 dedicated endpoints for common targets. Your parsing code does not change; only the line that fetches the page does.

import * as cheerio from "cheerio";

// Fetch through a scraper API that handles proxies and rendering.
const target = "https://quotes.toscrape.com/js/";
const res = await fetch("https://api.chocodata.com/v1?url=" + encodeURIComponent(target) + "&render=true", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
const $ = cheerio.load(await res.text());
// The HTML now arrives rendered, so the same selectors work.
console.log("quotes:", $("div.quote").length);

That is the division of labour I use: a fetch layer that survives anti-bot defenses, and Cheerio doing the parsing it is fast at. Treat the endpoint and parameters above as illustrative and check ChocoData’s own docs for exact request format.

When should you pick Cheerio over a browser tool?

Pick Cheerio when the data is already in the HTML you fetch, which covers most server-rendered sites, blogs, catalogues, and listing pages. It is the lightest, fastest option in the Node ecosystem for that job.

ToolRuns JavaScriptSpeedBest for
CheerioNoFastestStatic HTML, server-rendered pages
Puppeteer / PlaywrightYesSlowPages that build the DOM in the browser
Scraper API + CheerioYes (via API)FastBlocked sites, large jobs, mixed targets

My rule: start with fetch plus Cheerio, open the page source, and check whether the data you want is in it. If it is, you are done in 15 lines. If the page ships an empty shell and fills it with JavaScript, add a browser or route through an API and keep Cheerio for the parsing. For the same workflow in Python, see my BeautifulSoup tutorial and the broader Python scraping guide.

FAQ

Is Cheerio faster than Puppeteer?

Yes, by a wide margin. Cheerio parses an HTML string in memory with no browser, so it uses a few megabytes of RAM and finishes in milliseconds. Puppeteer launches a full Chromium instance per page. Use Cheerio when the data is already in the HTML, Puppeteer when a page needs JavaScript to render first.

Can Cheerio use XPath selectors?

No. Cheerio supports CSS selectors through the css-select engine, not XPath. If your extraction logic is written in XPath, either rewrite the expressions as CSS selectors or parse the same HTML with a library that speaks XPath, such as jsdom or Python's lxml.

Does Cheerio work in the browser?

Cheerio is built for Node.js server-side scripts. It ships as both ESM and CommonJS and runs anywhere Node runs. You can bundle it for the browser, but the usual reason to reach for it, scraping pages you fetch over HTTP, is a server-side job.

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.