Back to Blog
Guide February 26, 2026 · 4 min read

How to Take Screenshots and Generate PDFs in TypeScript and Deno

Puppeteer and Playwright both have TypeScript bindings, but both require a Chromium download and break in Deno and edge runtimes. Here's the typed alternative: fetch-based, zero Chromium, works in Node.js 18+, Deno, Bun, and Cloudflare Workers.

Puppeteer and Playwright both have TypeScript bindings, but they both require a bundled Chromium download and don't run in Deno, Cloudflare Workers, or any edge runtime. If you're working in a TypeScript-first environment and need screenshots or PDFs, the headless browser path breaks quickly.

Here's the typed alternative: fetch-based, zero Chromium, full TypeScript inference.

Typed client (works in Node.js, Deno, and Bun)

interface ScreenshotOptions {
  url: string;
  fullPage?: boolean;
  blockBanners?: boolean;
  blockAds?: boolean;
  darkMode?: boolean;
  viewportDevice?: string;
}

interface PdfOptions {
  url?: string;
  html?: string;
  blockBanners?: boolean;
}

class PageBoltClient {
  private readonly apiKey: string;
  private readonly baseUrl = "https://pagebolt.dev/api/v1";

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async screenshot(options: ScreenshotOptions): Promise<Uint8Array> {
    const res = await fetch(`${this.baseUrl}/screenshot`, {
      method: "POST",
      headers: {
        "x-api-key": this.apiKey,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(options),
    });
    if (!res.ok) throw new Error(`PageBolt error ${res.status}: ${await res.text()}`);
    return new Uint8Array(await res.arrayBuffer());
  }

  async pdf(options: PdfOptions): Promise<Uint8Array> {
    const res = await fetch(`${this.baseUrl}/pdf`, {
      method: "POST",
      headers: {
        "x-api-key": this.apiKey,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(options),
    });
    if (!res.ok) throw new Error(`PageBolt error ${res.status}: ${await res.text()}`);
    return new Uint8Array(await res.arrayBuffer());
  }
}

Deno — screenshot to file

// screenshot.ts
const pagebolt = new PageBoltClient(Deno.env.get("PAGEBOLT_API_KEY")!);

const image = await pagebolt.screenshot({
  url: "https://example.com",
  fullPage: true,
  blockBanners: true,
});

await Deno.writeFile("screenshot.png", image);
console.log(`Screenshot saved (${image.length} bytes)`);
deno run --allow-net --allow-env --allow-write screenshot.ts

Deno — PDF from HTML template

// invoice.ts
import { renderInvoice } from "./templates/invoice.ts";

const pagebolt = new PageBoltClient(Deno.env.get("PAGEBOLT_API_KEY")!);

const html = renderInvoice({ id: "INV-001", total: 149.99 });
const pdfBytes = await pagebolt.pdf({ html });

await Deno.writeFile("invoice.pdf", pdfBytes);

Hono (TypeScript web framework — runs on Deno, Bun, Cloudflare Workers)

import { Hono } from "hono";

const app = new Hono();
const pagebolt = new PageBoltClient(Deno.env.get("PAGEBOLT_API_KEY")!);

app.get("/invoices/:id/pdf", async (c) => {
  const id = c.req.param("id");
  const html = await renderInvoiceHtml(id);

  const pdfBytes = await pagebolt.pdf({ html });

  return new Response(pdfBytes, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": `attachment; filename="invoice-${id}.pdf"`,
    },
  });
});

Deno.serve(app.fetch);

The same Hono handler runs unchanged on Cloudflare Workers — fetch() is native, no Node.js compatibility layer needed.

Node.js with ts-node

// Using Node 18+ native fetch
import * as fs from "fs/promises";

const pagebolt = new PageBoltClient(process.env.PAGEBOLT_API_KEY!);

const image = await pagebolt.screenshot({
  url: "https://example.com",
  fullPage: true,
  blockBanners: true,
});

await fs.writeFile("screenshot.png", image);

Express + TypeScript controller

import express, { Request, Response } from "express";

const router = express.Router();
const pagebolt = new PageBoltClient(process.env.PAGEBOLT_API_KEY!);

router.get("/invoices/:id/pdf", async (req: Request, res: Response) => {
  const { id } = req.params;
  const html = await renderInvoiceHtml(id);

  const pdfBytes = await pagebolt.pdf({ html });

  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename="invoice-${id}.pdf"`);
  res.send(Buffer.from(pdfBytes));
});

Cloudflare Workers

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname.startsWith("/screenshot")) {
      const target = url.searchParams.get("url");
      if (!target) return new Response("Missing url param", { status: 400 });

      const pagebolt = new PageBoltClient(env.PAGEBOLT_API_KEY);
      const image = await pagebolt.screenshot({ url: target, blockBanners: true });

      return new Response(image, {
        headers: { "Content-Type": "image/png" },
      });
    }

    return new Response("Not found", { status: 404 });
  },
};

No npm install, no Chromium, no wrangler size limit issues from bundling a headless browser. The PageBoltClient class above is 30 lines and works identically in Deno, Bun, Node 18+, and Cloudflare Workers.

Get Started Free

100 requests/month, no credit card

Screenshots, PDFs, and video recordings from your TypeScript app — no Chromium download, full type inference, works in Deno, Bun, Node.js, and Cloudflare Workers.

Get Your Free API Key →