Deno Guide

Detect Website Tech Stacks in Deno with the StackPeek API

Published March 29, 2026 · 12 min read · By StackPeek

Deno is purpose-built for the kind of work that tech stack detection demands: TypeScript runs natively without a build step, fetch is a first-class global with no node-fetch shim needed, and the permission model lets you lock down network access to only the domains your script actually calls. If you are building a competitive intelligence tool, a sales enrichment pipeline, or a security audit workflow, Deno removes an entire layer of configuration that Node.js requires.

The StackPeek API returns structured JSON listing every detected framework, CMS, analytics tool, CDN, and hosting provider for any public URL. No headless browser, no Puppeteer, no Playwright dependency trees. One HTTP GET request, one JSON response, zero dependencies.

This guide covers ten production patterns: basic API calls, CLI tools with Cliffy, concurrent batch scanning, CSV/JSON export, Deno Fresh web apps, Deno Deploy serverless endpoints, Deno KV caching, and competitor analysis dashboards. Every example uses modern Deno APIs and runs with deno run.

1. Why Deno for Tech Stack Detection

Three features make Deno ideal for API-driven tech detection scripts:

2. Basic StackPeek API Call

Here is the simplest possible Deno script that detects a website's tech stack. Save it as detect.ts and run with deno run --allow-net detect.ts:

// detect.ts — Zero-dependency tech stack detection in Deno

interface Technology {
  name: string;
  category: string;
  confidence: number;
  version?: string;
  website?: string;
}

interface DetectResponse {
  url: string;
  technologies: Technology[];
  scanTime?: number;
}

const API_BASE =
  "https://us-central1-todd-agent-prod.cloudfunctions.net/stackpeekApi/api/v1/detect";

async function detectStack(targetUrl: string): Promise<DetectResponse> {
  const endpoint = `${API_BASE}?url=${encodeURIComponent(targetUrl)}`;
  const res = await fetch(endpoint);

  if (!res.ok) {
    throw new Error(`StackPeek API returned ${res.status}: ${res.statusText}`);
  }

  return res.json();
}

// Top-level await — no main() wrapper needed
const result = await detectStack("https://linear.app");

console.log(`Technologies detected on ${result.url}:\n`);
for (const tech of result.technologies) {
  const ver = tech.version ? ` v${tech.version}` : "";
  console.log(`  ${tech.name}${ver} — ${tech.category} (${tech.confidence}%)`);
}

That is 30 lines of TypeScript with zero imports and zero dependencies. The Technology and DetectResponse interfaces give you full autocompletion in VS Code. Top-level await means you do not need to wrap everything in an async IIFE like in Node.js.

3. CLI Tool with Cliffy

For a polished command-line experience, use Cliffy — the standard CLI framework for Deno. It provides argument parsing, help text, colored output, and subcommands via URL imports:

// stackpeek-cli.ts — Full CLI tool for batch tech stack scanning
import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts";
import { colors } from "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/colors.ts";
import { Table } from "https://deno.land/x/cliffy@v1.0.0-rc.3/table/mod.ts";

interface Technology {
  name: string;
  category: string;
  confidence: number;
  version?: string;
}

interface DetectResponse {
  url: string;
  technologies: Technology[];
}

const API =
  "https://us-central1-todd-agent-prod.cloudfunctions.net/stackpeekApi/api/v1/detect";

async function scan(url: string): Promise<DetectResponse> {
  const res = await fetch(`${API}?url=${encodeURIComponent(url)}`);
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}

await new Command()
  .name("stackpeek")
  .version("1.0.0")
  .description("Detect website tech stacks via the StackPeek API")
  .arguments("<urls...:string>")
  .option("-j, --json", "Output raw JSON instead of table")
  .option("-c, --category <cat:string>", "Filter by technology category")
  .action(async (options, ...urls: string[]) => {
    for (const url of urls) {
      const result = await scan(url);

      if (options.json) {
        console.log(JSON.stringify(result, null, 2));
        continue;
      }

      let techs = result.technologies;
      if (options.category) {
        techs = techs.filter((t) =>
          t.category.toLowerCase().includes(options.category!.toLowerCase())
        );
      }

      console.log(colors.cyan.bold(`\n${result.url}`));
      new Table()
        .header(["Technology", "Category", "Confidence", "Version"])
        .body(
          techs.map((t) => [
            colors.white.bold(t.name),
            t.category,
            `${t.confidence}%`,
            t.version ?? "—",
          ])
        )
        .border()
        .render();
    }
  })
  .parse(Deno.args);

Run it: deno run --allow-net stackpeek-cli.ts https://vercel.com https://stripe.com. Filter by category: --category framework. Get raw JSON: --json. Compile to a standalone binary: deno compile --allow-net stackpeek-cli.ts — the output is a single executable you can distribute without requiring Deno on the target machine.

4. Concurrent Scanning with Rate Limiting

When you need to scan hundreds of URLs, sequential requests are too slow. Use Promise.allSettled with a semaphore to control concurrency:

// batch-scan.ts — Concurrent scanning with rate limiting

interface Technology {
  name: string;
  category: string;
  confidence: number;
  version?: string;
}

interface DetectResponse {
  url: string;
  technologies: Technology[];
}

interface ScanResult {
  url: string;
  data?: DetectResponse;
  error?: string;
}

const API =
  "https://us-central1-todd-agent-prod.cloudfunctions.net/stackpeekApi/api/v1/detect";

function createSemaphore(max: number) {
  let count = 0;
  const queue: (() => void)[] = [];

  return {
    async acquire(): Promise<void> {
      if (count < max) {
        count++;
        return;
      }
      await new Promise<void>((resolve) => queue.push(resolve));
    },
    release(): void {
      count--;
      const next = queue.shift();
      if (next) {
        count++;
        next();
      }
    },
  };
}

async function scanWithLimit(
  urls: string[],
  concurrency = 5,
  delayMs = 200
): Promise<ScanResult[]> {
  const sem = createSemaphore(concurrency);

  const tasks = urls.map(async (url): Promise<ScanResult> => {
    await sem.acquire();
    try {
      // Rate-limit delay between requests
      await new Promise((r) => setTimeout(r, delayMs));
      const res = await fetch(`${API}?url=${encodeURIComponent(url)}`);
      if (!res.ok) {
        return { url, error: `HTTP ${res.status}` };
      }
      const data: DetectResponse = await res.json();
      return { url, data };
    } catch (err) {
      return { url, error: (err as Error).message };
    } finally {
      sem.release();
    }
  });

  const settled = await Promise.allSettled(tasks);
  return settled.map((s) =>
    s.status === "fulfilled"
      ? s.value
      : { url: "unknown", error: String(s.reason) }
  );
}

// Scan a list of competitor websites
const urls = [
  "https://linear.app",
  "https://vercel.com",
  "https://stripe.com",
  "https://notion.so",
  "https://figma.com",
  "https://github.com",
  "https://gitlab.com",
  "https://netlify.com",
];

console.log(`Scanning ${urls.length} URLs (concurrency: 5)...\n`);
const results = await scanWithLimit(urls, 5, 200);

for (const r of results) {
  if (r.error) {
    console.log(`[FAIL] ${r.url}: ${r.error}`);
  } else {
    const count = r.data!.technologies.length;
    console.log(`[OK]   ${r.url}: ${count} technologies detected`);
  }
}

The semaphore ensures at most 5 requests are in flight at once, and the delayMs parameter adds a small gap between launches to avoid burst-triggering rate limits. Promise.allSettled guarantees that one failing URL does not abort the entire batch — every URL gets a result or an error.

5. Exporting Results to CSV and JSON

After scanning a batch of URLs, you typically need the results in a format your team can use — a spreadsheet for the sales team, structured JSON for a database import:

// export.ts — Export scan results to CSV and JSON

interface ScanResult {
  url: string;
  data?: {
    url: string;
    technologies: {
      name: string;
      category: string;
      confidence: number;
      version?: string;
    }[];
  };
  error?: string;
}

function toCSV(results: ScanResult[]): string {
  const rows: string[] = ["url,technology,category,confidence,version"];

  for (const r of results) {
    if (!r.data) continue;
    for (const tech of r.data.technologies) {
      const version = tech.version ?? "";
      // Escape commas in field values
      rows.push(
        `"${r.url}","${tech.name}","${tech.category}",${tech.confidence},"${version}"`
      );
    }
  }
  return rows.join("\n");
}

function toJSON(results: ScanResult[]): string {
  const clean = results
    .filter((r) => r.data)
    .map((r) => ({
      url: r.url,
      technologies: r.data!.technologies,
    }));
  return JSON.stringify(clean, null, 2);
}

// Usage after batch scanning (assumes `results` from previous example)
// await Deno.writeTextFile("./results.csv", toCSV(results));
// await Deno.writeTextFile("./results.json", toJSON(results));
// console.log("Exported to results.csv and results.json");

// Standalone example with a single scan
const API =
  "https://us-central1-todd-agent-prod.cloudfunctions.net/stackpeekApi/api/v1/detect";

const res = await fetch(`${API}?url=${encodeURIComponent("https://stripe.com")}`);
const data = await res.json();

const results: ScanResult[] = [{ url: "https://stripe.com", data }];

await Deno.writeTextFile("./scan-results.csv", toCSV(results));
await Deno.writeTextFile("./scan-results.json", toJSON(results));

console.log("Exported scan-results.csv and scan-results.json");

Run with deno run --allow-net --allow-write export.ts. The --allow-write permission is required for file system writes. The CSV format uses quoted fields to handle commas in technology names. The JSON output is clean enough to pipe into jq or import into a database.

6. Deno Fresh Web App with Live Scanner

Deno Fresh is a full-stack web framework that ships zero JavaScript by default and uses islands architecture for interactivity. Here is a server-side route that performs a tech stack scan and renders the results:

// routes/scan.tsx — Deno Fresh route for live tech stack scanning
import { Handlers, PageProps } from "$fresh/server.ts";

interface Technology {
  name: string;
  category: string;
  confidence: number;
  version?: string;
}

interface ScanData {
  url: string;
  technologies: Technology[];
  error?: string;
}

const API =
  "https://us-central1-todd-agent-prod.cloudfunctions.net/stackpeekApi/api/v1/detect";

export const handler: Handlers<ScanData> = {
  async GET(req, ctx) {
    const url = new URL(req.url);
    const target = url.searchParams.get("url");

    if (!target) {
      return ctx.render({ url: "", technologies: [] });
    }

    try {
      const res = await fetch(
        `${API}?url=${encodeURIComponent(target)}`
      );
      if (!res.ok) {
        return ctx.render({
          url: target,
          technologies: [],
          error: `API returned ${res.status}`,
        });
      }
      const data = await res.json();
      return ctx.render(data);
    } catch (err) {
      return ctx.render({
        url: target,
        technologies: [],
        error: (err as Error).message,
      });
    }
  },
};

export default function ScanPage({ data }: PageProps<ScanData>) {
  return (
    <div class="max-w-2xl mx-auto p-8">
      <h1 class="text-3xl font-bold mb-6">Tech Stack Scanner</h1>
      <form method="GET" class="flex gap-2 mb-8">
        <input
          type="url"
          name="url"
          placeholder="https://example.com"
          value={data.url}
          class="flex-1 px-4 py-2 rounded bg-gray-800 text-white border border-gray-700"
          required
        />
        <button
          type="submit"
          class="px-6 py-2 bg-cyan-500 text-black font-semibold rounded"
        >
          Scan
        </button>
      </form>

      {data.error && <p class="text-red-400 mb-4">{data.error}</p>}

      {data.technologies.length > 0 && (
        <table class="w-full text-left">
          <thead>
            <tr class="border-b border-gray-700">
              <th class="py-2">Technology</th>
              <th class="py-2">Category</th>
              <th class="py-2">Confidence</th>
            </tr>
          </thead>
          <tbody>
            {data.technologies.map((t) => (
              <tr class="border-b border-gray-800">
                <td class="py-2 font-medium">{t.name}</td>
                <td class="py-2 text-gray-400">{t.category}</td>
                <td class="py-2">{t.confidence}%</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

The scan runs entirely server-side — no JavaScript is sent to the browser, so the page loads instantly. The form submits as a standard GET request, making results bookmarkable and shareable. Deploy the Fresh app to Deno Deploy with deployctl deploy --project=your-project main.ts.

7. Serverless Endpoint on Deno Deploy

If you just need a lightweight API proxy — perhaps to add caching, authentication, or CORS headers — Deno Deploy lets you ship a serverless function in a single file:

// main.ts — Deno Deploy serverless tech detection endpoint

const API =
  "https://us-central1-todd-agent-prod.cloudfunctions.net/stackpeekApi/api/v1/detect";

const CORS_HEADERS = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, OPTIONS",
  "Content-Type": "application/json",
};

Deno.serve({ port: 8000 }, async (req: Request): Promise<Response> => {
  if (req.method === "OPTIONS") {
    return new Response(null, { headers: CORS_HEADERS });
  }

  const url = new URL(req.url);
  const target = url.searchParams.get("url");

  if (!target) {
    return new Response(
      JSON.stringify({ error: "Missing ?url= parameter" }),
      { status: 400, headers: CORS_HEADERS }
    );
  }

  try {
    const res = await fetch(
      `${API}?url=${encodeURIComponent(target)}`
    );
    const data = await res.json();

    return new Response(JSON.stringify(data), {
      status: res.status,
      headers: {
        ...CORS_HEADERS,
        "Cache-Control": "public, max-age=3600",
      },
    });
  } catch (err) {
    return new Response(
      JSON.stringify({ error: (err as Error).message }),
      { status: 502, headers: CORS_HEADERS }
    );
  }
});

Deploy with deployctl deploy --project=my-tech-scanner main.ts. The endpoint adds CORS headers so you can call it from any frontend, and sets Cache-Control so CDN edges cache responses for an hour. Deno Deploy's free tier includes 1 million requests per month — more than enough for most use cases.

8. Caching with Deno KV

Deno KV is a built-in key-value store that works locally and on Deno Deploy. Use it to cache scan results so repeated lookups for the same domain are instant:

// cached-scanner.ts — StackPeek API with Deno KV caching

interface DetectResponse {
  url: string;
  technologies: { name: string; category: string; confidence: number; version?: string }[];
}

const API =
  "https://us-central1-todd-agent-prod.cloudfunctions.net/stackpeekApi/api/v1/detect";

const kv = await Deno.openKv();
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours

async function cachedDetect(targetUrl: string): Promise<DetectResponse> {
  const cacheKey = ["stackpeek", "scan", targetUrl];

  // Check cache first
  const cached = await kv.get<DetectResponse>(cacheKey);
  if (cached.value) {
    console.log(`[CACHE HIT] ${targetUrl}`);
    return cached.value;
  }

  // Cache miss — call the API
  console.log(`[CACHE MISS] ${targetUrl}`);
  const res = await fetch(`${API}?url=${encodeURIComponent(targetUrl)}`);
  if (!res.ok) {
    throw new Error(`API error: ${res.status}`);
  }

  const data: DetectResponse = await res.json();

  // Store in KV with expiration
  await kv.set(cacheKey, data, { expireIn: CACHE_TTL_MS });

  return data;
}

// First call hits the API, second call hits the cache
const result1 = await cachedDetect("https://stripe.com");
console.log(`Detected ${result1.technologies.length} technologies`);

const result2 = await cachedDetect("https://stripe.com");
console.log(`Detected ${result2.technologies.length} technologies (from cache)`);

Run with deno run --allow-net --unstable-kv cached-scanner.ts. The expireIn option automatically evicts stale entries after 24 hours. On Deno Deploy, KV is globally replicated — a scan cached in one region is available in all regions. This is ideal for building a tech detection proxy that responds in single-digit milliseconds for repeated queries.

9. Competitor Analysis Across Websites

One of the most practical use cases for StackPeek is comparing the tech stacks of competing websites. Here is a script that scans multiple competitors and produces a comparison matrix:

// competitor-analysis.ts — Compare tech stacks across competitor websites

interface Technology {
  name: string;
  category: string;
  confidence: number;
}

interface DetectResponse {
  url: string;
  technologies: Technology[];
}

const API =
  "https://us-central1-todd-agent-prod.cloudfunctions.net/stackpeekApi/api/v1/detect";

async function detect(url: string): Promise<DetectResponse> {
  const res = await fetch(`${API}?url=${encodeURIComponent(url)}`);
  if (!res.ok) throw new Error(`Failed: ${res.status}`);
  return res.json();
}

const competitors = [
  "https://linear.app",
  "https://asana.com",
  "https://monday.com",
  "https://clickup.com",
];

console.log("Scanning competitors...\n");

const results = await Promise.all(
  competitors.map(async (url) => {
    try {
      return await detect(url);
    } catch {
      return { url, technologies: [] as Technology[] };
    }
  })
);

// Build a matrix: technology name -> which competitors use it
const matrix = new Map<string, Set<string>>();

for (const r of results) {
  for (const tech of r.technologies) {
    if (!matrix.has(tech.name)) {
      matrix.set(tech.name, new Set());
    }
    matrix.get(tech.name)!.add(r.url);
  }
}

// Print comparison table
console.log("Technology Comparison Matrix");
console.log("=".repeat(80));

const header = ["Technology", ...competitors.map((u) => new URL(u).hostname)];
console.log(header.map((h) => h.padEnd(18)).join(""));
console.log("-".repeat(80));

for (const [tech, sites] of [...matrix.entries()].sort()) {
  const row = [
    tech.padEnd(18),
    ...competitors.map((c) => (sites.has(c) ? "  YES" : "  —").padEnd(18)),
  ];
  console.log(row.join(""));
}

// Summary: shared technologies
const shared = [...matrix.entries()]
  .filter(([, sites]) => sites.size === competitors.length)
  .map(([name]) => name);

console.log(`\nShared across all ${competitors.length} competitors:`);
for (const name of shared) {
  console.log(`  - ${name}`);
}

This is exactly the kind of analysis that sales teams pay BuiltWith $295/month for. With StackPeek at $9/month, you can build custom comparison reports that match your exact workflow — filtering by category, tracking changes over time, or feeding results into your CRM.

Scan any website's tech stack — 100 free scans/day. No API key required.

Try StackPeek Free

10. Pricing: StackPeek vs Wappalyzer vs BuiltWith

Every code example in this guide works on StackPeek's free tier. Here is how the pricing compares when you need to scale:

Provider Free Tier Paid Starting At Deno-Friendly
StackPeek 100 scans/day $9/month (5,000 scans) Yes — standard REST, native fetch
Wappalyzer 50 lookups/mo $250/month (50,000 lookups) Yes — REST API
BuiltWith None $295/month Yes — REST API

StackPeek's free tier alone gives you 3,000 scans per month — more than Wappalyzer's free plan by 60x. The $9/month plan is 27x cheaper than Wappalyzer and 32x cheaper than BuiltWith. For Deno developers, all three services work equally well since they all expose standard REST APIs. The difference is purely price.

FAQ

Does StackPeek require an API key for Deno?

No. The free tier (100 scans/day) requires no API key and no signup. Just call the endpoint with fetch. Paid plans use a simple API key passed as a query parameter or header. There is no Deno-specific SDK needed — the standard fetch API is all you need.

Can I compile a StackPeek CLI to a standalone binary with Deno?

Yes. Run deno compile --allow-net stackpeek-cli.ts and Deno produces a single executable binary for your platform. You can cross-compile for Linux, macOS, and Windows from any machine. The binary includes the Deno runtime, so the target machine does not need Deno installed. This is ideal for distributing internal tools across teams.

How do I handle rate limits when batch scanning in Deno?

Use a semaphore pattern with Promise.allSettled to limit concurrency. Set the concurrency to 5 and add a 200ms delay between requests. The StackPeek API returns a 429 status code when rate-limited, with a Retry-After header telling you how many seconds to wait. Build a retry loop that respects this header for production batch scanners.

Does Deno KV caching work on Deno Deploy?

Yes. Deno KV is natively supported on Deno Deploy with global replication. Data written in one region is available in all others within seconds. The expireIn option for automatic key expiration works identically in local development and production. Use --unstable-kv flag locally; it is stable on Deno Deploy.

What technologies can StackPeek detect?

StackPeek detects 120+ technologies across categories including JavaScript frameworks (React, Vue, Angular, Svelte, Next.js), CMS platforms (WordPress, Shopify, Webflow), analytics tools (Google Analytics, Segment, Mixpanel), CDNs (Cloudflare, Fastly, AWS CloudFront), hosting providers, CSS frameworks, and server-side languages. The detection engine analyzes HTTP headers, HTML meta tags, JavaScript globals, and DOM patterns.