DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to Take Screenshots in TypeScript with the PageBolt API

How to Take Screenshots in TypeScript with the PageBolt API

You're building a TypeScript app that needs screenshots. You have a few options:

  1. Self-host Puppeteer — manage browser processes, handle crashes, scale in production
  2. Use a screenshot API — one endpoint, type-safe, no infrastructure

If you've chosen option 2, here's how to integrate PageBolt with full TypeScript support.

The Basic Pattern: Type-Safe API Calls

import * as fs from 'fs';

// Define your request shape
interface ScreenshotRequest {
  url: string;
  format: 'png' | 'jpeg' | 'webp';
  width?: number;
  height?: number;
  fullPage?: boolean;
  blockBanners?: boolean;
}

// Define the API response
interface ScreenshotResponse {
  success: boolean;
  message?: string;
}

// Define possible errors
type ScreenshotError =
  | { code: 'INVALID_URL'; message: string }
  | { code: 'API_ERROR'; status: number; message: string }
  | { code: 'FILE_WRITE_ERROR'; message: string };

async function takeScreenshot(
  request: ScreenshotRequest,
  outputPath: string,
  apiKey: string
): Promise<ScreenshotResponse | ScreenshotError> {
  try {
    const response = await fetch('https://api.pagebolt.dev/v1/screenshot', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        url: request.url,
        format: request.format,
        width: request.width || 1280,
        height: request.height || 720,
        fullPage: request.fullPage ?? false,
        blockBanners: request.blockBanners ?? true
      })
    });

    if (!response.ok) {
      return {
        code: 'API_ERROR',
        status: response.status,
        message: `Screenshot API returned ${response.status}`
      };
    }

    // Get binary response as ArrayBuffer
    const buffer = await response.arrayBuffer();

    // Write to disk using Buffer
    fs.writeFileSync(outputPath, Buffer.from(buffer));

    return { success: true };
  } catch (error) {
    if (error instanceof Error) {
      return {
        code: 'FILE_WRITE_ERROR',
        message: error.message
      };
    }
    throw error;
  }
}

// Usage
const result = await takeScreenshot(
  {
    url: 'https://example.com',
    format: 'png',
    fullPage: true,
    blockBanners: true
  },
  './screenshot.png',
  'YOUR_API_KEY'
);

if (result.success) {
  console.log('Screenshot saved');
} else {
  console.error(`Error: ${result.message}`);
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  1. Defines strict types for request, response, and errors
  2. Uses response.arrayBuffer() to handle binary PNG data
  3. Writes to disk using Buffer.from()
  4. Returns typed error objects instead of throwing

Production Pattern: Class-Based API Client

For larger projects, wrap the API in a client class:

import * as fs from 'fs';
import * as path from 'path';

interface ScreenshotOptions {
  url: string;
  format?: 'png' | 'jpeg' | 'webp';
  width?: number;
  height?: number;
  fullPage?: boolean;
  blockBanners?: boolean;
  blockAds?: boolean;
  darkMode?: boolean;
}

class PageBoltClient {
  private apiKey: string;
  private apiBase = 'https://api.pagebolt.dev/v1';

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

  async screenshot(
    options: ScreenshotOptions,
    outputPath?: string
  ): Promise<{ data: Buffer; path?: string }> {
    const payload = {
      url: options.url,
      format: options.format || 'png',
      width: options.width || 1280,
      height: options.height || 720,
      fullPage: options.fullPage ?? false,
      blockBanners: options.blockBanners ?? true,
      blockAds: options.blockAds ?? false,
      darkMode: options.darkMode ?? false
    };

    const response = await fetch(`${this.apiBase}/screenshot`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(payload)
    });

    if (!response.ok) {
      throw new Error(
        `PageBolt API error: ${response.status} ${response.statusText}`
      );
    }

    const buffer = Buffer.from(await response.arrayBuffer());

    if (outputPath) {
      fs.mkdirSync(path.dirname(outputPath), { recursive: true });
      fs.writeFileSync(outputPath, buffer);
      return { data: buffer, path: outputPath };
    }

    return { data: buffer };
  }

  async batch(
    urls: string[],
    outputDir: string,
    options?: Partial<ScreenshotOptions>
  ): Promise<{ path: string; url: string }[]> {
    const results: { path: string; url: string }[] = [];

    for (const url of urls) {
      const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.png`;
      const filepath = path.join(outputDir, filename);

      const result = await this.screenshot(
        { ...options, url },
        filepath
      );

      results.push({
        url,
        path: result.path || filepath
      });
    }

    return results;
  }
}

// Usage
const client = new PageBoltClient(process.env.PAGEBOLT_API_KEY!);

// Single screenshot
const { data, path: filePath } = await client.screenshot(
  {
    url: 'https://example.com',
    fullPage: true
  },
  './screenshots/homepage.png'
);

console.log(`Screenshot saved to ${filePath}`);

// Batch screenshots
const urls = [
  'https://example.com',
  'https://example.com/about',
  'https://example.com/pricing'
];

const results = await client.batch(urls, './screenshots');
console.log(`Batch complete: ${results.length} screenshots`);
Enter fullscreen mode Exit fullscreen mode

Node.js Integration: Screenshot on Route

Use PageBolt in an Express server:

import express, { Request, Response } from 'express';
import * as fs from 'fs';
import * as path from 'path';

const app = express();
const apiKey = process.env.PAGEBOLT_API_KEY!;

interface ScreenshotBody {
  url: string;
  format?: 'png' | 'jpeg' | 'webp';
  fullPage?: boolean;
}

app.post('/api/screenshot', async (req: Request<{}, {}, ScreenshotBody>, res: Response) => {
  const { url, format = 'png', fullPage = true } = req.body;

  if (!url) {
    return res.status(400).json({ error: 'url is required' });
  }

  try {
    const response = await fetch('https://api.pagebolt.dev/v1/screenshot', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        url,
        format,
        fullPage,
        width: 1280,
        height: 720,
        blockBanners: true
      })
    });

    if (!response.ok) {
      return res.status(response.status).json({
        error: `Screenshot API error: ${response.statusText}`
      });
    }

    const buffer = Buffer.from(await response.arrayBuffer());

    // Return image directly
    res.setHeader('Content-Type', `image/${format}`);
    res.setHeader('Content-Length', buffer.length);
    res.send(buffer);
  } catch (error) {
    console.error('Screenshot error:', error);
    res.status(500).json({
      error: error instanceof Error ? error.message : 'Unknown error'
    });
  }
});

app.listen(3000, () => {
  console.log('Server listening on :3000');
  console.log('POST /api/screenshot with { url, format?, fullPage? }');
});
Enter fullscreen mode Exit fullscreen mode

Test it:

curl -X POST http://localhost:3000/api/screenshot \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","fullPage":true}' \
  > example.png
Enter fullscreen mode Exit fullscreen mode

Deno Integration: Modern Async TypeScript

Deno handles typed fetch natively. No Buffer needed — Deno has Uint8Array:

// deno.json
{
  "imports": {
    "std/": "https://deno.land/std@0.208.0/"
  }
}
Enter fullscreen mode Exit fullscreen mode
// screenshot.ts (Deno)
interface ScreenshotOptions {
  url: string;
  format?: 'png' | 'jpeg' | 'webp';
  width?: number;
  height?: number;
  fullPage?: boolean;
  blockBanners?: boolean;
}

async function takeScreenshot(
  options: ScreenshotOptions,
  outputPath: string,
  apiKey: string
): Promise<void> {
  const response = await fetch('https://api.pagebolt.dev/v1/screenshot', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url: options.url,
      format: options.format || 'png',
      width: options.width || 1280,
      height: options.height || 720,
      fullPage: options.fullPage ?? false,
      blockBanners: options.blockBanners ?? true
    })
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  // Deno: use arrayBuffer() then write
  const buffer = await response.arrayBuffer();
  await Deno.writeFile(outputPath, new Uint8Array(buffer));

  console.log(`Screenshot saved to ${outputPath}`);
}

// Usage
const apiKey = Deno.env.get('PAGEBOLT_API_KEY') || '';

await takeScreenshot(
  {
    url: 'https://example.com',
    fullPage: true,
    blockBanners: true
  },
  './screenshot.png',
  apiKey
);
Enter fullscreen mode Exit fullscreen mode

Run it:

export PAGEBOLT_API_KEY=your_key_here
deno run --allow-net --allow-write screenshot.ts
Enter fullscreen mode Exit fullscreen mode

Error Handling Patterns

Distinguish between recoverable and fatal errors:

type ApiErrorCode =
  | 'INVALID_URL'
  | 'TIMEOUT'
  | 'INVALID_FORMAT'
  | 'RATE_LIMIT'
  | 'API_ERROR';

class ApiError extends Error {
  constructor(
    public code: ApiErrorCode,
    public statusCode?: number,
    message?: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function takeScreenshotWithRetry(
  url: string,
  apiKey: string,
  maxRetries = 3
): Promise<Buffer> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch('https://api.pagebolt.dev/v1/screenshot', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ url, format: 'png' })
      });

      if (response.status === 429) {
        // Rate limit: wait and retry
        if (attempt < maxRetries) {
          const waitMs = 1000 * Math.pow(2, attempt - 1);
          console.log(`Rate limited. Retrying in ${waitMs}ms...`);
          await new Promise(resolve => setTimeout(resolve, waitMs));
          continue;
        }
        throw new ApiError('RATE_LIMIT', 429);
      }

      if (!response.ok) {
        throw new ApiError('API_ERROR', response.status);
      }

      return Buffer.from(await response.arrayBuffer());
    } catch (error) {
      if (error instanceof ApiError && error.code === 'RATE_LIMIT') {
        if (attempt === maxRetries) throw error;
        continue;
      }
      throw error;
    }
  }

  throw new Error('Max retries exceeded');
}
Enter fullscreen mode Exit fullscreen mode

Real Use Case: Marketing Email Screenshots

Generate screenshots for email campaigns:

import { PageBoltClient } from './pagebolt-client';

interface EmailVariant {
  name: string;
  url: string;
}

async function generateEmailProofs(
  variants: EmailVariant[],
  outputDir: string
): Promise<void> {
  const client = new PageBoltClient(process.env.PAGEBOLT_API_KEY!);

  console.log(`Generating ${variants.length} screenshots...`);

  for (const variant of variants) {
    const outputPath = `${outputDir}/${variant.name}.png`;

    await client.screenshot(
      {
        url: variant.url,
        width: 600, // Email width
        fullPage: true,
        blockBanners: true,
        blockAds: true
      },
      outputPath
    );

    console.log(`✓ ${variant.name}`);
  }

  console.log(`All screenshots saved to ${outputDir}`);
}

// Usage
await generateEmailProofs(
  [
    { name: 'variant-a', url: 'https://example.com?variant=a' },
    { name: 'variant-b', url: 'https://example.com?variant=b' },
    { name: 'variant-c', url: 'https://example.com?variant=c' }
  ],
  './email-proofs'
);
Enter fullscreen mode Exit fullscreen mode

Pricing

Plan Requests/Month Cost Best For
Free 100 $0 Learning, low-volume projects
Starter 5,000 $29 Small teams, moderate use
Growth 25,000 $79 Production apps, frequent calls
Scale 100,000 $199 High-volume automation

Summary

  • ✅ Full TypeScript support with proper types
  • response.arrayBuffer() for binary PNG handling
  • Buffer.from() for Node.js, Uint8Array for Deno
  • ✅ Error handling with typed error codes
  • ✅ Works in Node.js, Deno, and Express
  • ✅ Batch processing for multiple URLs
  • ✅ No browser management overhead

Get started free: pagebolt.dev — 100 requests/month, no credit card required.

Top comments (0)