DEV Community

Boehner
Boehner

Posted on

How to Detect Any Website's Tech Stack in 30 Lines of Node.js

How to Detect Any Website's Tech Stack in 30 Lines of Node.js

I was putting together a competitor analysis for a client last month. They wanted to know not just what competitors were doing visually, but what they were running under the hood — CMS, framework, analytics, chat widgets, A/B testing tools, the whole stack.

The old workflow: open each URL in a browser, run Wappalyzer or BuiltWith, copy the results into a spreadsheet. For 20 competitors, that's 45 minutes of clicking. And you have to redo it every quarter because stacks change.

Here's how I automated the whole thing.

What We're Building

A script that takes a list of competitor URLs and outputs a clean report showing the detected tech stack for each one — frameworks, CMS platforms, CDNs, analytics tools, chat widgets, whatever's detectable.

The output looks like this:

[snapapi.tech]
  - Next.js
  - Vercel
  - Google Analytics
  - Intercom

[competitor2.com]
  - WordPress
  - WooCommerce
  - Cloudflare
  - HubSpot

[competitor3.io]
  - React
  - AWS CloudFront
  - Segment
  - Drift
Enter fullscreen mode Exit fullscreen mode

One script run, all 20 competitors, done in about 10 seconds.

The Stack

We're using SnapAPI — it runs a headless browser on their end and returns structured page data including detected technologies. No Playwright, no Puppeteer, no browser dependency to manage.

The analyze endpoint returns a technologies array with everything it detects. It uses the same detection logic as popular browser extensions, but server-side.

The Code

// stack-detector.js
const https = require('https');

const SNAPAPI_KEY = process.env.SNAPAPI_KEY;
const BASE_URL = 'https://snapapi.tech';

const competitors = [
  'https://competitor1.com',
  'https://competitor2.com',
  'https://competitor3.io',
  // add up to 50 URLs
];

function analyzePage(url) {
  return new Promise((resolve, reject) => {
    const endpoint = `${BASE_URL}/analyze?url=${encodeURIComponent(url)}&apiKey=${SNAPAPI_KEY}`;
    https.get(endpoint, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        try {
          resolve({ url, result: JSON.parse(data) });
        } catch (e) {
          reject(e);
        }
      });
    }).on('error', reject);
  });
}

async function detectStacks(urls) {
  const results = await Promise.all(urls.map(analyzePage));

  for (const { url, result } of results) {
    const domain = new URL(url).hostname;
    const techs = result.technologies || [];

    console.log(`\n[${domain}]`);
    if (techs.length === 0) {
      console.log('  No technologies detected');
    } else {
      techs.forEach(t => console.log(`  - ${t}`));
    }
  }
}

detectStacks(competitors).catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Install nothing. Run it:

SNAPAPI_KEY=your_key node stack-detector.js
Enter fullscreen mode Exit fullscreen mode

That's it.

What the Analyze Endpoint Actually Returns

The analyze endpoint returns a lot more than just tech detection. Here's the shape:

{
  "url": "https://example.com",
  "title": "Example — Home",
  "description": "...",
  "technologies": ["React", "Cloudflare", "Google Analytics"],
  "headings": ["H1: Hero headline", "H2: Features", ...],
  "links": [...],
  "buttons": ["Get started free", "See pricing"],
  "cta": "Get started free",
  "word_count": 847,
  "load_time_ms": 1243,
  "page_type": "landing"
}
Enter fullscreen mode Exit fullscreen mode

So in the same call that detects their tech stack, you're also getting:

  • What their primary CTA is
  • Their full heading structure
  • Page word count
  • Page type classification

That's a full competitive brief from a single API call.

Extending It: Save to a File

If you want to save results for comparison over time:

const fs = require('fs');

async function detectStacks(urls) {
  const results = await Promise.all(urls.map(analyzePage));
  const report = {};

  for (const { url, result } of results) {
    const domain = new URL(url).hostname;
    report[domain] = {
      technologies: result.technologies || [],
      cta: result.cta || null,
      word_count: result.word_count || 0,
      checked_at: new Date().toISOString(),
    };
  }

  const filename = `stack-report-${Date.now()}.json`;
  fs.writeFileSync(filename, JSON.stringify(report, null, 2));
  console.log(`\nSaved: ${filename}`);
  return report;
}
Enter fullscreen mode Exit fullscreen mode

Run this weekly and diff the outputs — you'll catch when a competitor switches from WordPress to Webflow, adds a new chat tool, or drops their A/B testing platform.

Scaling to 50+ Competitors: Batch Mode

For larger lists, use the batch endpoint to avoid hitting rate limits:

function batchAnalyze(urls) {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({ urls, apiKey: SNAPAPI_KEY });
    const options = {
      hostname: 'snapapi.tech',
      path: '/batch/analyze',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
      },
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => resolve(JSON.parse(data)));
    });

    req.on('error', reject);
    req.write(body);
    req.end();
  });
}
Enter fullscreen mode Exit fullscreen mode

The batch endpoint processes all URLs in parallel on their end and returns the full results array. For 50 competitors, this runs in about the same time as analyzing one URL serially — it's all concurrent.

What This Actually Tells You

Once you have this running weekly, patterns become obvious fast:

Stack drift — A competitor switching from Intercom to Crisp usually means they're cost-cutting. From GA to Segment usually means they're getting serious about data. These are signal.

Conversion optimization signals — Adding an A/B testing tool (Optimizely, VWO, AB Tasty) shows up in the technologies array. If three competitors all add A/B testing in the same quarter, something is happening in the market.

Infrastructure changes — Moving from shared hosting to Cloudflare + Vercel often precedes a redesign or traffic push. Worth watching.

Tool proliferation — Sometimes you'll see a competitor with 12 different analytics and marketing tools running simultaneously. That's usually a sign of disorganized growth or a recent acquisition.

The Manual Alternative

For reference, here's what this workflow looked like before:

  1. Open each competitor URL in browser
  2. Run Wappalyzer (browser extension, only works in foreground)
  3. Screenshot the results
  4. Tab over to spreadsheet
  5. Type in what you saw
  6. Repeat x20

About 45 minutes for 20 competitors. Monthly: 3 hours. Yearly: 36 hours just staring at Wappalyzer popups.

The script above does the same job in under 15 seconds and saves the output automatically.

One Step Further: Full Competitive Intelligence on Autopilot

The tech stack script is useful, but it's one piece of the picture. If you want the full view — their headlines, CTAs, what changed since last week, and a plain-English summary of what you should care about — that's what we built BusinessPulse for.

It runs every Monday, scrapes your competitors with the SnapAPI analyze endpoint (tech stack included), then passes the structured data to Claude. You get a brief in your inbox that reads like: "Competitor A updated their pricing page. They added a free trial CTA and dropped their enterprise tier. Competitor B just added Intercom — looks like they're investing in support. Competitor C switched from Wix to Webflow, probably a redesign incoming."

Set it up once, ignore it until Monday. Demo at snapapi.tech/businesspulse/demo-v2.


The tech stack script above is free to use. Get a SnapAPI key at snapapi.tech — free tier covers 100 calls/month, which is enough to scan 20 competitors five times.

What are you tracking on your competitors? Drop it in the comments — always curious what verticals people are applying this to.

Top comments (0)