<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lucas Rainett</title>
    <description>The latest articles on DEV Community by Lucas Rainett (@lucasrainett).</description>
    <link>https://dev.to/lucasrainett</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F186421%2Fa1c28d78-95ed-4aee-9f57-59cfaf823a63.jpeg</url>
      <title>DEV Community: Lucas Rainett</title>
      <link>https://dev.to/lucasrainett</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucasrainett"/>
    <language>en</language>
    <item>
      <title>How Pair Programming and Mob Programming can make you a better AI Developer</title>
      <dc:creator>Lucas Rainett</dc:creator>
      <pubDate>Mon, 27 Apr 2026 20:23:42 +0000</pubDate>
      <link>https://dev.to/lucasrainett/how-pair-programming-and-mob-programming-made-me-a-better-ai-developer-did</link>
      <guid>https://dev.to/lucasrainett/how-pair-programming-and-mob-programming-made-me-a-better-ai-developer-did</guid>
      <description>&lt;p&gt;TLDR: Pair programming and Mob programming skills are 100% transferable to AI coding.&lt;/p&gt;

&lt;p&gt;Most developers I know are frustrated with AI coding tools. They write a prompt, get something weird back, try again, and eventually give up entirely and re-write everything manually.&lt;/p&gt;

&lt;p&gt;I am not having that experience. And I've spent time thinking about why.&lt;/p&gt;

&lt;p&gt;I think the answer is the experience working in collaborative environments with &lt;a href="https://en.wikipedia.org/wiki/Pair_programming" rel="noopener noreferrer"&gt;pair programming&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Mob_programming" rel="noopener noreferrer"&gt;mob programming&lt;/a&gt;, and the accountability I take for people's understanding of what I try to say. When I'm pairing, when I'm teaching, when I'm prompting an AI, I'm always evaluating if the other side is missing something, and if yes, how do I provide it?&lt;/p&gt;

&lt;p&gt;Let me explain what I mean.&lt;/p&gt;




&lt;h2&gt;
  
  
  The insight I got from teaching
&lt;/h2&gt;

&lt;p&gt;I started working as a teacher at 21 years old, as a secondary activity on top of working as a developer, mostly to force myself out of my comfort zone as an extremely introverted person.&lt;/p&gt;

&lt;p&gt;Working with students from a variety of backgrounds and learning speeds taught me something interesting: knowledge is a step-by-step process, where every new step relies on the step before.&lt;/p&gt;

&lt;p&gt;There is always a dependency. There is always a step underneath the step you think you're teaching.&lt;/p&gt;

&lt;p&gt;So if we try to explain a complex topic to someone who is missing the base to understand it, they will not absorb the information. And it is the speaker's fault. The speaker is accountable for identifying which step the audience is on, and building up from there.&lt;/p&gt;

&lt;p&gt;Every time someone doesn't understand something, it's because they're missing the previous step.&lt;/p&gt;

&lt;p&gt;As a speaker, the job isn't to repeat the explanation louder. It's to identify the missing step, then fill it using an example anchored in something they already understood.&lt;/p&gt;




&lt;h2&gt;
  
  
  The insight I got from pair and mob programming
&lt;/h2&gt;

&lt;p&gt;I'll be honest: when I first heard about &lt;a href="https://en.wikipedia.org/wiki/Pair_programming" rel="noopener noreferrer"&gt;pair programming&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Mob_programming" rel="noopener noreferrer"&gt;mob programming&lt;/a&gt;, I thought it was a waste of time.&lt;/p&gt;

&lt;p&gt;Multiple developers, one keyboard, one screen. I couldn't see how that was more efficient than everyone working in parallel.&lt;/p&gt;

&lt;p&gt;In New Zealand I worked alongside the person who literally wrote the book on mob programming. And it took time to come around. What changed my mind wasn't a single moment. It was repeated exposure to what happens when a room of smart people are all looking at the same problem together. And also seeing a project from start to end with mob programming, the value will not show in one or two sessions. It shows after a while, when the team flow is high and the bugs are low.&lt;/p&gt;

&lt;p&gt;The navigator is not just watching. When the driver goes sideways, you step in, the same way I stepped in with students. You find what they are missing, and you give it to them.&lt;/p&gt;

&lt;p&gt;I became a genuine advocate. I ran coding sessions at every company I worked at after that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The insight I got from AI development
&lt;/h2&gt;

&lt;p&gt;Since my first interaction with AI development, I treat the agent as a fellow new joiner. A person with some technical experience, but no clue about what we are building here.&lt;/p&gt;

&lt;p&gt;I share links to documentation, point to code examples in the project to follow as references, and proactively ask if it has enough context before we start.&lt;/p&gt;

&lt;p&gt;Most frustrated developers treat AI coding agent like a search engine. They type what they want, get something back, find it wrong, and conclude the tool doesn't work.&lt;/p&gt;

&lt;p&gt;But if you act as the navigator from pair programming and provide the context the driver is missing, catch when they've gone off-track, and redirect, the experience is genuinely different.&lt;/p&gt;

&lt;p&gt;And most importantly, not only share what we are building, but why we are building it. The rationale behind every decision is context the AI doesn't have unless you give it.&lt;/p&gt;




&lt;p&gt;Here is the uncomfortable truth: if the AI doesn't understand you, it is your fault. Be better at communicating what you want.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;I built a project where 99% of the code was AI-generated, with every AI output committed raw so what was produced by the AI is visible in the git history. I commit as-is, before touching anything, then review and correct in a separate commit.&lt;/p&gt;

&lt;p&gt;Three-step cycle, for each separated feature: prompt, raw AI result, manual review. This mirrors the mob programming rhythm exactly. The driver runs, the navigator reviews, the mob catches what drifted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Brief the AI before you write a single line
&lt;/h3&gt;

&lt;p&gt;Have you heard about "Project Kickoff"?&lt;/p&gt;

&lt;p&gt;Before writing any code in this project, I asked the AI to draft Architecture Decision Records (short documents that capture what you decided and why) covering all sorts of concerns: library choices, deployment strategy, layer boundaries, testing plan, code style, and database schema. I reviewed and corrected each one MANUALLY.&lt;/p&gt;

&lt;p&gt;This is what I used to do with students before a new concept. You don't just start with the new thing. You make sure all the prerequisite pieces are fresh, named, and linked. Then you move forward.&lt;/p&gt;

&lt;p&gt;When it was time to implement, the AI had precise documented context for every decision it would need to make, and why each decision was made. This is particularly useful in the long term as every new session can re-build the context about the project&lt;/p&gt;

&lt;p&gt;And later during coding, if the AI did something unexpected, that was the signal for a new ADR.&lt;/p&gt;

&lt;p&gt;The ADRs served two purposes at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For me&lt;/strong&gt; forced me to think through decisions before touching the keyboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For the AI&lt;/strong&gt; provided constraints that guided every subsequent generation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Anchor prompts to concepts the AI already knows
&lt;/h3&gt;

&lt;p&gt;One prompt from this project read:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Create the minimum set of configuration files to allow a complete AWS deployment. This technique is called hello world in production, and should allow each future step to be deployed to the cloud, instead of only doing deploy after the code is complete."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't just say "set up AWS deployment." I gave it a named technique and explained why the technique matters. I anchored the new task to a concept already in its knowledge base.&lt;/p&gt;

&lt;p&gt;Same as the classroom. You don't ask for the new thing cold. You connect it to something they already know.&lt;/p&gt;




&lt;h2&gt;
  
  
  The habits that transfer directly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Identify what step the AI is missing, not just what it got wrong.&lt;/strong&gt; When output is bad, don't just rewrite the prompt. Ask yourself: what context did it not have? What assumption did it make that I haven't corrected? Provide that, with an example it already knows. The problem was never the output, it is always the missing context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warm up the session before asking for anything.&lt;/strong&gt; Don't start with implementation. Start with documentation, decisions, constraints. Load the context deliberately, the way you'd brief a new pair partner on a system before sitting down together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference external resources, not just internal ones.&lt;/strong&gt; Point the AI at library docs, example repositories, other files in the same codebase. Same as mob programming: you don't expect the driver to know everything. You hand them the reference they need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat every raw output as a first draft.&lt;/strong&gt; 100% of the code in this project should be reviewed. The AI was always the driver. I was always the navigator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Work in small, verifiable steps.&lt;/strong&gt; Each prompt built on the last. I didn't ask for the whole system at once. Each step was deployable and verifiable before moving forward.&lt;/p&gt;




&lt;h2&gt;
  
  
  What most developers are skipping
&lt;/h2&gt;

&lt;p&gt;The most common failure pattern I see: one big prompt, one big output, it's wrong, conclusion: AI coding doesn't work.&lt;/p&gt;

&lt;p&gt;The missing step isn't prompt engineering. It's knowing how to collaborate incrementally, how to provide context progressively, catch drift early, and course-correct without restarting from scratch.&lt;/p&gt;

&lt;p&gt;Those are pairing skills. Most developers never developed them, because most teams don't pair seriously, and most developers, as I was, are skeptical about the value until they see it work.&lt;/p&gt;

&lt;p&gt;If you want to get better at AI-assisted development, the fastest path isn't learning more prompting techniques. It's learning to pair program well, and then applying the same instincts to a new kind of partner.&lt;/p&gt;

&lt;p&gt;The developers getting the most out of AI tools right now are not the ones who know the most tricks. They are the ones who can communicate clearly. Who can take what is in their head and make it legible to another mind without losing the intent along the way.&lt;br&gt;
That is not a new skill. Teachers build it. Pair programmers build it. Anyone who has ever had to explain a complex system to a new joiner and watched them get it, or not get it, and adjusted, they have been building it.&lt;br&gt;
The tool changed. The skill didn't.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Credit to &lt;a href="https://www.linkedin.com/in/markpearl" rel="noopener noreferrer"&gt;Mark Pearl&lt;/a&gt;, author of &lt;a href="https://pragprog.com/titles/mpmob/code-with-the-wisdom-of-the-crowd/" rel="noopener noreferrer"&gt;Code with the Wisdom of the Crowd&lt;/a&gt;, for the mob programming experience that, eventually, proved me wrong.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What if you could reverse a template engine?</title>
      <dc:creator>Lucas Rainett</dc:creator>
      <pubDate>Fri, 10 Apr 2026 16:23:32 +0000</pubDate>
      <link>https://dev.to/lucasrainett/what-if-you-could-reverse-a-template-engine-5nk</link>
      <guid>https://dev.to/lucasrainett/what-if-you-could-reverse-a-template-engine-5nk</guid>
      <description>&lt;p&gt;I've been building software for over 20 years across banking, healthcare, financial SaaS, and media. At &lt;a href="https://www.stuff.co.nz" rel="noopener noreferrer"&gt;Stuff&lt;/a&gt;, one of New Zealand's largest news platforms, I worked on systems that consumed and produced structured content at scale. In banking and healthcare, I dealt with legacy systems that predate REST APIs entirely: fixed-format financial reports, statement exports, internal tools that print structured text to stdout because that was the interface in 2003 and nobody has touched it since.&lt;/p&gt;

&lt;p&gt;Back in 2012 I worked as a remote contractor for SITA, a Canadian company in the aviation industry, parsing CSV flight fare data and loading it into a database. Hundreds of files, each with a slightly different layout depending on the source system, each needing to be mapped to the same schema. We wrote a lot of custom parsing code in Java for what was, in the end, just structured text with a consistent shape.&lt;/p&gt;

&lt;p&gt;The problem of "here is some structured text, I need the data inside it" is one of the most persistent problems in software. And the tooling for it, honestly, has not kept up.&lt;/p&gt;

&lt;p&gt;A few years ago it found me again. I was working on a side project that needed to extract structured data from a set of web pages. The pages were clearly generated from a template. Every one of them had the structure. Just different data.&lt;/p&gt;

&lt;p&gt;So I reached for the standard tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  The cheerio phase
&lt;/h2&gt;

&lt;p&gt;If you've scraped HTML with &lt;a href="https://cheerio.js.org/" rel="noopener noreferrer"&gt;cheerio&lt;/a&gt;, you know the drill. You open DevTools, hunt for a CSS selector that's stable enough to rely on, and write something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.product-title h1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.price-box .current-price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.specs-table tr:nth-child(2) td:last-child&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine for one or two fields. But the page I was working with had nested data: a list of items, each with their own sub-fields. Now I'm writing loops, mapping over &lt;code&gt;$(".review-list li")&lt;/code&gt;, extracting children by index, trimming whitespace everywhere, and the code looks nothing like the data I'm trying to produce.&lt;/p&gt;

&lt;p&gt;And it's fragile. The site updates a class name, rearranges a div, and your selectors silently return empty strings. You don't find out until your pipeline starts producing garbage.&lt;/p&gt;




&lt;h2&gt;
  
  
  The puppeteer detour
&lt;/h2&gt;

&lt;p&gt;Puppeteer can fetch the page, but it still doesn't help with extracting the data. On top of that, you're spinning up a full headless Chromium instance to read a page that is, in the end, just text. The startup time, the memory, the flakiness of &lt;code&gt;waitForSelector&lt;/code&gt; all felt like overkill for what I was trying to do.&lt;/p&gt;

&lt;p&gt;In enterprise and regulated environments this gets worse. Security policies, sandboxed build agents, and locked-down CI pipelines often make running a headless browser simply not an option. I've been in those environments. A zero-dependency, pure-text approach is not just nicer, it's sometimes the only viable one.&lt;/p&gt;

&lt;p&gt;Both tools (cheerio and puppeteer) had another limitation: they're HTML-only. But the world is full of structured text that isn't HTML. Log files. Emails. Markdown documents. Fixed-format financial reports. API responses rendered as plain text. I wanted something that could work on all of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  A different way to think about it
&lt;/h2&gt;

&lt;p&gt;At some point, frustrated and staring at yet another brittle CSS selector, I caught myself thinking about the pages differently.&lt;/p&gt;

&lt;p&gt;EJS is a popular JavaScript template engine. Servers use it to generate HTML by calling something like &lt;code&gt;ejs.render(template, data)&lt;/code&gt;, where the template is an HTML file with placeholders and the data is the object that fills them in. The template already encodes all the rules for how data maps to text.&lt;/p&gt;

&lt;p&gt;If the forward direction exists (template + data = text) then the reverse must be possible too. Text + template = data.&lt;/p&gt;

&lt;p&gt;I had never seen this idea anywhere. I googled around, found nothing. The concept felt obvious in retrospect but apparently nobody had built it, at least not for general text.&lt;/p&gt;

&lt;p&gt;I made a mental note and moved on. Life got busy. The idea sat in the back of my head for years.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building it (with a little help)
&lt;/h2&gt;

&lt;p&gt;Recently, with AI tooling finally good enough to act as a real coding partner, I sat down and built it in a day.&lt;/p&gt;

&lt;p&gt;The first version was naive. I just tried to turn EJS template literals into a regex and match against the rendered string. It worked for simple cases and completely fell apart the moment loops entered the picture.&lt;/p&gt;

&lt;p&gt;The breakthrough was realising I needed an AST first.&lt;/p&gt;

&lt;p&gt;AST stands for Abstract Syntax Tree, a concept borrowed from how compilers work. When a compiler reads your code, it doesn't just scan it left to right as a string of characters. It first parses the text into a tree structure that represents the meaning of the code: this block is a function, inside it there's a loop, inside the loop there's a variable assignment, and so on. Each node in the tree is a meaningful piece of the program, and the relationships between nodes capture how those pieces nest and relate to each other. The word "abstract" means it strips away irrelevant details like whitespace and punctuation and keeps only the structure that matters.&lt;/p&gt;

&lt;p&gt;For reverse-ejs, this meant I couldn't just scan the EJS template as text and replace tags with regex patterns on the fly. I needed to first parse the whole template into a tree that captured its real structure: here is a literal HTML chunk, here is a variable output tag, here is a loop that contains more literal HTML and more variable tags inside it, here is a conditional with two branches. Only once that tree existed could I walk it and generate the right regex for each node, with loops becoming repeating capture groups and conditionals becoming alternatives.&lt;/p&gt;

&lt;p&gt;Once that clicked, everything else fell into place. Loops became repeating capture groups. Conditionals became alternations. Nested objects became dot-notation in the capture group names.&lt;/p&gt;

&lt;p&gt;The moment it first worked end-to-end, I pasted a product page HTML, wrote a quick EJS template, ran the function, and got back a clean JSON object. I knew it was right.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;reverse-ejs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reverseEjs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reverse-ejs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
&amp;lt;div class="product"&amp;gt;
  &amp;lt;h1&amp;gt;&amp;lt;%= name %&amp;gt;&amp;lt;/h1&amp;gt;
  &amp;lt;span class="price"&amp;gt;$&amp;lt;%= price %&amp;gt;&amp;lt;/span&amp;gt;
  &amp;lt;% reviews.forEach(review =&amp;gt; { %&amp;gt;
  &amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;%= review.author %&amp;gt;&amp;lt;/strong&amp;gt;: &amp;lt;%= review.text %&amp;gt;&amp;lt;/li&amp;gt;
  &amp;lt;% }) %&amp;gt;
&amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* fetched from the site */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reverseEjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flexibleWhitespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   name: "Sony WH-1000XM5",&lt;/span&gt;
&lt;span class="c1"&gt;//   price: "348.00",&lt;/span&gt;
&lt;span class="c1"&gt;//   reviews: [&lt;/span&gt;
&lt;span class="c1"&gt;//     { author: "Alice", text: "Best headphones I've ever owned." },&lt;/span&gt;
&lt;span class="c1"&gt;//     { author: "Bob", text: "Great sound quality." }&lt;/span&gt;
&lt;span class="c1"&gt;//   ]&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template is the schema. You write what the page looks like, and you get back the data that produced it.&lt;/p&gt;




&lt;h2&gt;
  
  
  It works on more than HTML
&lt;/h2&gt;

&lt;p&gt;The library never assumes its input is HTML. It just matches text against a template, which means it works on anything with a consistent structure.&lt;/p&gt;

&lt;p&gt;Remember the SITA project from 2012? The fare files were not simple one-row-per-record CSVs. Each fare was spread across four consecutive lines, one for each record type: route, pricing, rules, and dates. They looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;RTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;AC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;YYZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;GRU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;YLOWCA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;OW&lt;/span&gt;
&lt;span class="k"&gt;PRC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;542.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;98.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;45.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;685.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAD&lt;/span&gt;
&lt;span class="k"&gt;RUL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;21&lt;/span&gt;&lt;span class="k"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;
&lt;span class="k"&gt;DAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-03-01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-05-31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-01-15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-05-25&lt;/span&gt;

&lt;span class="k"&gt;RTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;AC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;YYZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;GRU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;BLOWCA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;OW&lt;/span&gt;
&lt;span class="k"&gt;PRC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;489.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;98.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;45.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;632.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAD&lt;/span&gt;
&lt;span class="k"&gt;RUL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;14&lt;/span&gt;&lt;span class="k"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;YES&lt;/span&gt;
&lt;span class="k"&gt;DAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-03-01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-05-31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-01-15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-05-25&lt;/span&gt;

&lt;span class="k"&gt;RTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;AC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;YVR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;LHR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;YFLEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;RT&lt;/span&gt;
&lt;span class="k"&gt;PRC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1240.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;187.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;95.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1522.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CAD&lt;/span&gt;
&lt;span class="k"&gt;RUL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;YES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NO&lt;/span&gt;
&lt;span class="k"&gt;DAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-04-01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-06-30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-02-01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2012-06-15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A standard CSV parser reads one row at a time. It has no concept of a record that spans multiple lines with different structures. You end up writing a state machine: read a line, check the prefix, decide which object you are building, accumulate fields, flush when you hit the next &lt;code&gt;RTE&lt;/code&gt; line. We wrote exactly that, and it was fiddly and brittle.&lt;/p&gt;

&lt;p&gt;With reverse-ejs, the whole file becomes a single template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;% fares.forEach(f =&amp;gt; { %&amp;gt;RTE,&amp;lt;%= f.carrier %&amp;gt;,&amp;lt;%= f.origin %&amp;gt;,&amp;lt;%= f.destination %&amp;gt;,&amp;lt;%= f.fareBasis %&amp;gt;,&amp;lt;%= f.cabin %&amp;gt;,&amp;lt;%= f.direction %&amp;gt;
PRC,&amp;lt;%= f.baseFare %&amp;gt;,&amp;lt;%= f.taxes %&amp;gt;,&amp;lt;%= f.surcharge %&amp;gt;,&amp;lt;%= f.total %&amp;gt;,&amp;lt;%= f.currency %&amp;gt;
RUL,&amp;lt;%= f.advancePurchase %&amp;gt;,&amp;lt;%= f.minStay %&amp;gt;,&amp;lt;%= f.maxStay %&amp;gt;,&amp;lt;%= f.refundable %&amp;gt;,&amp;lt;%= f.changeable %&amp;gt;
DAT,&amp;lt;%= f.validFrom %&amp;gt;,&amp;lt;%= f.validTo %&amp;gt;,&amp;lt;%= f.firstSale %&amp;gt;,&amp;lt;%= f.lastSale %&amp;gt;
&amp;lt;% }) %&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;reverseEjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fareFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;baseFare&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;taxes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;surcharge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   fares: [&lt;/span&gt;
&lt;span class="c1"&gt;//     { carrier: "AC", origin: "YYZ", destination: "GRU", fareBasis: "YLOWCA",&lt;/span&gt;
&lt;span class="c1"&gt;//       cabin: "Y", direction: "OW", baseFare: 542.00, taxes: 98.40,&lt;/span&gt;
&lt;span class="c1"&gt;//       surcharge: 45.00, total: 685.40, currency: "CAD",&lt;/span&gt;
&lt;span class="c1"&gt;//       advancePurchase: "21D", minStay: "7", maxStay: "30",&lt;/span&gt;
&lt;span class="c1"&gt;//       refundable: "NO", changeable: "NO",&lt;/span&gt;
&lt;span class="c1"&gt;//       validFrom: "2012-03-01", validTo: "2012-05-31",&lt;/span&gt;
&lt;span class="c1"&gt;//       firstSale: "2012-01-15", lastSale: "2012-05-25" },&lt;/span&gt;
&lt;span class="c1"&gt;//     ...&lt;/span&gt;
&lt;span class="c1"&gt;//   ]&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No state machine. No prefix checking. No wondering whether the &lt;code&gt;RUL&lt;/code&gt; line you just read belongs to the fare above or below it. The template describes the shape of the data and the library figures the rest out. I genuinely wish this had existed back then.&lt;/p&gt;

&lt;p&gt;And it goes further than CSV. In financial services I've seen reports like this coming out of legacy systems — no API, no JSON, just text that has been printed the same way for fifteen years:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACCOUNT SUMMARY - 2026-04-10
================================
Account:  ACC-00123      John Smith
Currency: USD

TRANSACTIONS
------------
2026-04-08  PAYMENT RECEIVED        +  5,000.00   Balance:  12,430.50
2026-04-09  WIRE TRANSFER OUT       -  1,200.00   Balance:  11,230.50
2026-04-10  SERVICE FEE             -     15.00   Balance:  11,215.50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what extracting it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`ACCOUNT SUMMARY - &amp;lt;%= date %&amp;gt;
================================
Account:  &amp;lt;%= accountId %&amp;gt;      &amp;lt;%= accountName %&amp;gt;
Currency: &amp;lt;%= currency %&amp;gt;

TRANSACTIONS
------------
&amp;lt;% transactions.forEach(t =&amp;gt; { %&amp;gt;&amp;lt;%= t.date %&amp;gt;  &amp;lt;%= t.description %&amp;gt;  &amp;lt;%= t.amount %&amp;gt;   Balance:  &amp;lt;%= t.balance %&amp;gt;
&amp;lt;% }) %&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;reverseEjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   date: "2026-04-10",&lt;/span&gt;
&lt;span class="c1"&gt;//   accountId: "ACC-00123",&lt;/span&gt;
&lt;span class="c1"&gt;//   accountName: "John Smith",&lt;/span&gt;
&lt;span class="c1"&gt;//   currency: "USD",&lt;/span&gt;
&lt;span class="c1"&gt;//   transactions: [&lt;/span&gt;
&lt;span class="c1"&gt;//     { date: "2026-04-08", description: "PAYMENT RECEIVED       ", amount: "+  5,000.00", balance: "12,430.50" },&lt;/span&gt;
&lt;span class="c1"&gt;//     { date: "2026-04-09", description: "WIRE TRANSFER OUT      ", amount: "-  1,200.00", balance: "11,230.50" },&lt;/span&gt;
&lt;span class="c1"&gt;//     { date: "2026-04-10", description: "SERVICE FEE            ", amount: "-     15.00", balance: "11,215.50" },&lt;/span&gt;
&lt;span class="c1"&gt;//   ]&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log files are another common case. Say you have a file like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[INFO]  2026-04-10T08:12:01Z api-gateway: server started on port 3000
[INFO]  2026-04-10T08:12:45Z auth-service: user login successful
[WARN]  2026-04-10T08:13:10Z api-gateway: response time exceeded threshold
[ERROR] 2026-04-10T08:14:22Z db-service: connection pool exhausted
[INFO]  2026-04-10T08:15:05Z auth-service: token refreshed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One template, the whole file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;% entries.forEach(e =&amp;gt; { %&amp;gt;[&amp;lt;%= e.level %&amp;gt;] &amp;lt;%= e.timestamp %&amp;gt; &amp;lt;%= e.service %&amp;gt;: &amp;lt;%= e.message %&amp;gt;
&amp;lt;% }) %&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;reverseEjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   entries: [&lt;/span&gt;
&lt;span class="c1"&gt;//     { level: "INFO",  timestamp: "2026-04-10T08:12:01Z", service: "api-gateway",  message: "server started on port 3000" },&lt;/span&gt;
&lt;span class="c1"&gt;//     { level: "INFO",  timestamp: "2026-04-10T08:12:45Z", service: "auth-service", message: "user login successful" },&lt;/span&gt;
&lt;span class="c1"&gt;//     { level: "WARN",  timestamp: "2026-04-10T08:13:10Z", service: "api-gateway",  message: "response time exceeded threshold" },&lt;/span&gt;
&lt;span class="c1"&gt;//     { level: "ERROR", timestamp: "2026-04-10T08:14:22Z", service: "db-service",   message: "connection pool exhausted" },&lt;/span&gt;
&lt;span class="c1"&gt;//     { level: "INFO",  timestamp: "2026-04-10T08:15:05Z", service: "auth-service", message: "token refreshed" },&lt;/span&gt;
&lt;span class="c1"&gt;//   ]&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea applies to emails, Markdown documents, CLI output. Anything you can describe with an EJS template.&lt;/p&gt;




&lt;h2&gt;
  
  
  The practical bits
&lt;/h2&gt;

&lt;p&gt;There are a few features worth knowing about for real-world use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compiled templates:&lt;/strong&gt; when you're processing many pages against the same template, compile once and reuse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compileTemplate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reverse-ejs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compiled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compileTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;compiled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Safe mode:&lt;/strong&gt; for scraping pipelines where some pages won't match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reverseEjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;safe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// fall back to your cheerio extractor&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Type coercion:&lt;/strong&gt; extracted values come back as strings by default, but you can ask for numbers, booleans, or dates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;reverseEjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inStock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero dependencies. 20KB. TypeScript-native. Runs in Node.js, Bun, Deno, and the browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the limitations, because they matter for deciding when to reach for this versus cheerio.&lt;/p&gt;

&lt;p&gt;If the site uses heavy JavaScript rendering (a React SPA, for example), you still need Puppeteer or Playwright to get the HTML first. reverse-ejs takes over after that.&lt;/p&gt;

&lt;p&gt;If two variables appear side by side with no text between them (&lt;code&gt;&amp;lt;%= firstName %&amp;gt;&amp;lt;%= lastName %&amp;gt;&lt;/code&gt;), the split point is ambiguous and the library returns them as a single combined value. Add a separator in the template and you're fine.&lt;/p&gt;

&lt;p&gt;And if the site randomly changes its HTML structure between renders (A/B tests, CMS quirks, injected banners), the match may fail. &lt;code&gt;safe: true&lt;/code&gt; combined with a cheerio fallback handles this gracefully.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;I built a playground where you can paste your HTML and template and see the extraction live in your browser. No install, no account, your HTML never leaves your device:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://lucasrainett.github.io/reverse-ejs/" rel="noopener noreferrer"&gt;lucasrainett.github.io/reverse-ejs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If it solves a problem for you, I'd genuinely appreciate a star on GitHub. It's the clearest signal that the idea is useful to people beyond me.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/lucasrainett/reverse-ejs" rel="noopener noreferrer"&gt;github.com/lucasrainett/reverse-ejs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And I'm curious: have you run into this problem before? What have you been using to extract structured data from template-rendered pages? Drop a comment, I'd love to know what cases I haven't thought of yet.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>ejs</category>
      <category>template</category>
    </item>
  </channel>
</rss>
