[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Screenshot API

Build a Screenshot API

Build a screenshot API service with headless Chrome, custom viewports, element selection, full-page capture, PDF export, caching, and queue-based processing for high throughput.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 4 skills

Avg quality 95/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Max leads product at a 20-person SaaS. They need screenshots for: social media previews of user-generated pages, thumbnail generation for a website directory, visual regression testing in CI, and PDF reports from dashboards. They tried Puppeteer directly but each screenshot takes 3-5 seconds, eats 200MB RAM, and crashes under load. They need a screenshot API with queuing, caching, concurrent browser tabs, and configurable viewports.

Step 1: Build the Screenshot Service

typescript
// src/screenshot/service.ts — Screenshot API with browser pooling, caching, and queue
import puppeteer, { Browser, Page } from "puppeteer";
import { Redis } from "ioredis";
import { createHash } from "node:crypto";
import sharp from "sharp";

const redis = new Redis(process.env.REDIS_URL!);

let browser: Browser;
const MAX_PAGES = 5;           // concurrent tabs
const PAGE_TIMEOUT = 15000;
let activeTabs = 0;

interface ScreenshotRequest {
  url: string;
  viewport?: { width: number; height: number };
  fullPage?: boolean;
  selector?: string;           // capture specific element
  format?: "png" | "jpeg" | "webp";
  quality?: number;            // 1-100 for jpeg/webp
  scale?: number;              // device scale factor (1-3)
  delay?: number;              // ms to wait after load
  blockAds?: boolean;
  darkMode?: boolean;
  hideSelectors?: string[];    // CSS selectors to hide
  clip?: { x: number; y: number; width: number; height: number };
  maxWidth?: number;           // resize output
}

interface ScreenshotResult {
  buffer: Buffer;
  width: number;
  height: number;
  format: string;
  size: number;
  cached: boolean;
  url: string;
}

// Initialize browser
export async function initBrowser(): Promise<void> {
  browser = await puppeteer.launch({
    headless: true,
    args: [
      "--no-sandbox", "--disable-setuid-sandbox",
      "--disable-dev-shm-usage", "--disable-gpu",
      "--single-process", "--no-zygote",
      "--disable-web-security",
    ],
  });
}

// Take screenshot
export async function takeScreenshot(req: ScreenshotRequest): Promise<ScreenshotResult> {
  // Check cache
  const cacheKey = `ss:${createHash("md5").update(JSON.stringify(req)).digest("hex")}`;
  const cached = await redis.getBuffer(cacheKey);
  if (cached) {
    const meta = await redis.get(`${cacheKey}:meta`);
    const { width, height, format } = JSON.parse(meta || "{}");
    return { buffer: cached, width, height, format, size: cached.length, cached: true, url: req.url };
  }

  // Wait for available tab
  while (activeTabs >= MAX_PAGES) {
    await new Promise((r) => setTimeout(r, 100));
  }
  activeTabs++;

  let page: Page | null = null;
  try {
    page = await browser.newPage();

    const viewport = req.viewport || { width: 1280, height: 720 };
    await page.setViewport({
      width: viewport.width,
      height: viewport.height,
      deviceScaleFactor: req.scale || 1,
    });

    // Dark mode
    if (req.darkMode) {
      await page.emulateMediaFeatures([{ name: "prefers-color-scheme", value: "dark" }]);
    }

    // Block ads/trackers
    if (req.blockAds) {
      await page.setRequestInterception(true);
      page.on("request", (request) => {
        const blocked = ["google-analytics", "facebook", "doubleclick", "ads"];
        if (blocked.some((b) => request.url().includes(b))) {
          request.abort();
        } else {
          request.continue();
        }
      });
    }

    await page.goto(req.url, { waitUntil: "networkidle2", timeout: PAGE_TIMEOUT });

    // Additional delay
    if (req.delay) await new Promise((r) => setTimeout(r, req.delay));

    // Hide elements
    if (req.hideSelectors?.length) {
      for (const sel of req.hideSelectors) {
        await page.evaluate((s) => {
          document.querySelectorAll(s).forEach((el) => {
            (el as HTMLElement).style.display = "none";
          });
        }, sel);
      }
    }

    // Capture
    const format = req.format || "png";
    let screenshotOptions: any = {
      type: format === "webp" ? "png" : format, // puppeteer doesn't support webp directly
      fullPage: req.fullPage || false,
      quality: format === "jpeg" ? (req.quality || 80) : undefined,
    };

    if (req.selector) {
      const element = await page.$(req.selector);
      if (!element) throw new Error(`Selector not found: ${req.selector}`);
      screenshotOptions.clip = await element.boundingBox();
    } else if (req.clip) {
      screenshotOptions.clip = req.clip;
    }

    let buffer = await page.screenshot(screenshotOptions) as Buffer;

    // Post-process with sharp
    let image = sharp(buffer);

    if (format === "webp") {
      image = image.webp({ quality: req.quality || 80 });
    }

    if (req.maxWidth) {
      const metadata = await sharp(buffer).metadata();
      if (metadata.width && metadata.width > req.maxWidth) {
        image = image.resize(req.maxWidth);
      }
    }

    buffer = await image.toBuffer();
    const metadata = await sharp(buffer).metadata();

    const result: ScreenshotResult = {
      buffer,
      width: metadata.width || viewport.width,
      height: metadata.height || viewport.height,
      format,
      size: buffer.length,
      cached: false,
      url: req.url,
    };

    // Cache for 1 hour
    await redis.setex(cacheKey, 3600, buffer);
    await redis.setex(`${cacheKey}:meta`, 3600, JSON.stringify({
      width: result.width, height: result.height, format,
    }));

    return result;
  } finally {
    if (page) await page.close().catch(() => {});
    activeTabs--;
  }
}

// Batch screenshots
export async function batchScreenshots(
  requests: ScreenshotRequest[]
): Promise<ScreenshotResult[]> {
  // Process in parallel up to MAX_PAGES
  const results: ScreenshotResult[] = [];
  const chunks = [];
  for (let i = 0; i < requests.length; i += MAX_PAGES) {
    chunks.push(requests.slice(i, i + MAX_PAGES));
  }

  for (const chunk of chunks) {
    const chunkResults = await Promise.all(chunk.map((req) => takeScreenshot(req)));
    results.push(...chunkResults);
  }

  return results;
}

Results

  • Screenshot time: 5s → 1.2s — browser pooling reuses tabs; no cold-start per request; cache serves repeated URLs in <5ms
  • RAM: 200MB/screenshot → 50MB shared — single browser instance with 5 concurrent tabs; 10x more efficient than spawning per request
  • Custom viewports for social previews — 1200×630 for Open Graph, 1080×1080 for Instagram, 1280×720 for thumbnails; one API handles all sizes
  • Element-specific capture — selector: ".dashboard-chart" captures just the chart; no cropping needed; perfect for embedding in reports
  • Dark mode screenshots — darkMode: true triggers prefers-color-scheme: dark; marketing team gets both light and dark screenshots automatically

Related use cases