← All posts

Cheerio Web Scraping Guide: jQuery-Style Parsing in Node

Quick answer: Cheerio is a fast, server-side HTML parser for Node.js that gives you jQuery-style selectors without a browser. Install it with npm install cheerio, fetch a page's HTML with fetch, load that HTML with cheerio.load, then query elements with the familiar $("selector") syntax and read text or attributes. It only parses static HTML: it does not run JavaScript, so for client-rendered pages you pair it with a real browser like Playwright or a rendering fetch API.

If BeautifulSoup is the go-to parser for Python people, Cheerio is its Node counterpart. It implements a large slice of the jQuery API on the server, so if you have ever written $(".item").text() in a browser, you already know most of Cheerio. It is small, quick, and does one job: parse HTML and let you query it.

Install Cheerio

Cheerio is a single npm package. Modern Node has fetch built in, so you often need nothing else to download pages.

npm install cheerio

If you are on an older Node version without global fetch, add a client such as node-fetch or axios. Everything below assumes Node 18 or newer.

Load HTML

The core function is cheerio.load, which parses an HTML string and returns a $ function you query with.

import * as cheerio from "cheerio";

const html = `
  <ul class="items">
    <li class="item"><a href="/a">First</a></li>
    <li class="item"><a href="/b">Second</a></li>
  </ul>`;

const $ = cheerio.load(html);
console.log($("li.item").length);        // 2
console.log($("li.item").first().text()); // First

$ behaves like the jQuery you remember. That is the whole appeal: the mental model transfers directly.

Fetch a Real Page, Then Parse It

In practice you download the HTML first, then load it. Cheerio does not fetch anything itself.

import * as cheerio from "cheerio";

const res = await fetch("https://example.com", {
  headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" },
});
const html = await res.text();
const $ = cheerio.load(html);

console.log($("title").text());
console.log($("h1").first().text());

Setting a real User-Agent matters. A lot of sites serve a stripped page or a block to a default Node fetch agent.

Selectors and Extraction

Cheerio supports the CSS selectors you already know, plus jQuery traversal helpers.

$("div.product");                 // by tag and class
$("a[href^='https']");            // attribute starts-with
$("ul.items > li");               // direct children
$("table tr td:nth-child(2)");    // second cell of each row

Reading data off matched elements:

const el = $("a.detail").first();
el.text();              // visible text
el.attr("href");        // an attribute
el.html();              // inner HTML
$(el).find("span").text(); // nested query

To collect data across many elements, use .each or .map. I prefer .map(...).get() because it returns a plain array.

const titles = $("h2.title")
  .map((i, el) => $(el).text().trim())
  .get();

Traversal helpers cover the "data lives next to a landmark" cases:

const label = $("span:contains('Price')");
const value = label.next().text();          // the element right after
const cell = $("td:contains('Total')").parent().find("td").last().text();

A Complete Example

This scrapes a listing, pulls a title, price, and link from each card, skips incomplete cards, and writes a JSON file. It uses fetch to download and Cheerio to parse.

import * as cheerio from "cheerio";
import { writeFile } from "node:fs/promises";

async function scrape(url) {
  const res = await fetch(url, {
    headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" },
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  const $ = cheerio.load(await res.text());
  const rows = [];

  $("div.product-card").each((i, card) => {
    const $card = $(card);
    const title = $card.find("h2").text().trim();
    const price = $card.find(".price").text().trim();
    const link = $card.find("a").attr("href");
    if (!title || !price || !link) return; // skip incomplete
    rows.push({ title, price, url: link });
  });

  return rows;
}

const data = await scrape("https://example.com/products");
await writeFile("products.json", JSON.stringify(data, null, 2));
console.log(`wrote ${data.length} items`);

The shape is the same one you would use with any parser: fetch, load, select, guard against missing fields, collect.

The Limit: Static HTML Only

Cheerio parses the HTML string you give it and nothing more. It has no JavaScript engine, no DOM events, no rendering. If a page builds its content client-side (most React, Vue, and Svelte apps), the HTML you fetch is a near-empty shell and your selectors match nothing.

The diagnostic is quick: if $("div.product-card").length is 0 but the products are clearly visible in your browser, the content is rendered by JavaScript after load.

Two solid ways forward:

  1. Render first, then parse. Use Playwright to load the page in a real engine, grab the rendered HTML with page.content(), and hand that string to cheerio.load. You keep Cheerio's clean querying while getting rendered content. See the Playwright web scraping guide and the broader JavaScript web scraping guide.
  2. Use a fetch API that renders for you. link.sc handles the browser and proxies server-side and returns clean content, so there is nothing to render locally:
curl https://link.sc/v1/fetch \
  -H "Authorization: Bearer lsc_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/products", "format": "markdown"}'

For a page that renders server-side you can also request its HTML and still parse with Cheerio if you want the raw structure. The full parameter list is in the link.sc docs.

Cheerio vs a Browser

Need Best fit
Parse static HTML fast, low memory Cheerio
Query with familiar jQuery selectors Cheerio
Run JavaScript, click, scroll Playwright
Render at scale without babysitting browsers Fetch API

Cheerio is the fast, cheap layer. Playwright is the rendering layer. A fetch API is both, run for you as a service. Many robust scrapers use Cheerio for parsing and delegate rendering to one of the other two.

A Note on Ethics

Scrape public data only, honor robots.txt, set a timeout, and space out your requests so you are not overloading a server. If the site publishes an API, prefer it: it is more stable and it is what the operator intends.

Wrapping Up

Cheerio brings jQuery-style HTML parsing to Node with almost no overhead. Load HTML, query with selectors you already know, extract text and attributes, and guard against missing fields. Just remember it never runs JavaScript: pair it with a renderer for client-side pages and it stays a sharp, dependable tool.


Need rendered HTML without running a browser in Node? link.sc fetches and renders any URL, then returns clean markdown or JSON. Try it free with 500 credits a month.