Release candidate — 1.0.0-rc.6
StitchAPI

Catch a silent selector rename in scraped HTML

Scrape an HTML page into a structured object with transform, then drift the structured shape against its schema so a markup or selector rename becomes a hard contract error instead of a silently missing field.

Task

You scrape a page that has no API — a catalog provider returns HTML, and you parse it into a structured object with a selector. The danger is silent: the provider renames a CSS class or moves a cell, your selector quietly stops matching, and the field comes back undefined. Nothing throws; downstream code keeps running on corrupted data until someone notices a ranking is wrong. You want that markup rename to surface the moment it happens, as a loud error on the structured shape — not the raw HTML.

Example

The response pipeline is transform → pick → validation, so transform reshapes the HTML into a structured object before drift ever runs — drift then validates the parsed value, not the raw markup. Declare score as required in the schema: its loss is the breakage you want caught.

import { ,  } from 'stitchapi';
import {  } from 'zod';

// A trivial hand-rolled scraper: the `score` selector keys off `td.score` —
// exactly the class a markup rename silently breaks.
function (: unknown): { : <string, unknown>[] } {
    const  = ();
    const  = .(/<tr[^>]*class="row1"[^>]*>/i).(1);
    const  = .(() => {
        const : <string, unknown> = {};
        const  = /<td[^>]*class="title"[^>]*>([^<]+)<\/td>/i.();
        const  = /<td[^>]*class="score"[^>]*>\s*(\d+)\s*<\/td>/i.(
            ,
        )?.[1];
        if () ['title'] = [1]!.();
        if ( !== ) ['score'] = (); // omitted when the selector misses
        return ;
    });
    return {  };
}

const  = ({
    : 'https://api.example.com',
    : '/catalog',
    : , // HTML string -> { items: [...] }
    : 'items',
    : (
        // `score` is REQUIRED — its loss is the breakage we want caught.
        // A required field that goes missing throws (`STITCH_DRIFT`, `change: 'invalid'`).
        .(.({ : .(), : .() })),
    ),
});

const  = await ();

How it works

Each call parses the page and validates the transformed shape — [{ title, score }], not the HTML body — against the schema. No baseline to generate or commit: the schema is the contract.

When the provider renames the score cell's class (scorerank), the selector stops matching and scrape drops score from each item. Because the schema requires score, validation sees a missing required field and throws STITCH_DRIFT — a finding with { level: 'error', change: 'invalid', path: '[].score' }. The path is the proof: [].score exists only on the parsed object, so a finding on it can only come from drift inspecting the transform output, not the raw markup. The silent undefined is now a loud, immediate failure.

Other non-fatal soft-drift findings (undeclared, coerced, defaulted) ride the event stream without throwing — you can watch them before any becomes a hard failure.

Declaring score required is the whole trick. Drift honors the schema's optionality: if score were .optional(), a response without it would validate clean and the rename would slip past — which is correct for a field that genuinely may be absent. Make a field required precisely when its disappearance is a bug.

Anti-pattern: do not drift the raw HTML body and skip transform. Snapshotting the markup makes every cosmetic edit — a reordered attribute, a whitespace change — look like drift, burying the one rename that matters. Parse to the structured shape first, then rely on required fields in the schema to catch the breakage you actually depend on.

See also

On this page