Tutorial

How to Capture Website Screenshots with Node.js in 2026

How to Capture Website Screenshots with Node.js in 2026

January 31, 2026 • 5 min read

MacBook Pro showing code editor with programming setup

Photo by Safar Safarov on Unsplash

Taking screenshots of websites programmatically is a common need for developers. Whether you're building link previews, generating thumbnails, or creating visual testing tools, you need a reliable way to capture web pages.

Method 1: Puppeteer (Self-hosted)

const puppeteer = require('puppeteer');

async function takeScreenshot(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url);
  await page.screenshot({ path: 'screenshot.png' });
  await browser.close();
}

Cons: Chrome updates break code, memory issues, requires infrastructure.

curl "https://api.snapapi.pics/v1/screenshot?url=example.com" \
  -H "X-API-Key: your_key" \
  --output screenshot.png

Pros: No infrastructure, automatic scaling, built-in features, free tier available.

Try SnapAPI Free

Get 200 free screenshots per month. No credit card required.

Get Started Free →

Start Capturing for Free

200 screenshots/month. Screenshots, PDF, scraping, and video recording. No credit card required.

Get Free API Key →

Complete Node.js screenshot API tutorial

This guide covers everything from your first screenshot to production patterns: error handling, retries, caching, and processing screenshots at scale in Node.js.

Installation and setup

npm install @sleywill/snapapi-js
# or use fetch directly — no dependencies required

Your first screenshot (3 lines)

import SnapAPI from '@sleywill/snapapi-js';
const client = new SnapAPI(process.env.SNAPAPI_KEY);
const { url: imageUrl } = await client.screenshot({ url: 'https://example.com' });
console.log(imageUrl); // https://storage.snapapi.pics/screenshots/abc123.jpeg

Full-page screenshot with custom options

const screenshot = await client.screenshot({
  url: 'https://example.com',
  format: 'png',           // png | jpeg | webp | avif
  full_page: true,         // capture entire scrollable page
  width: 1440,             // viewport width
  device: 'iphone_15',    // or any of 26 device presets
  wait_until: 'networkidle', // wait for all network requests
  delay: 1000,             // extra ms after load
  block_ads: true,         // block ads and cookie banners
  dark_mode: false,        // force dark/light mode
});

Express.js: Screenshot endpoint

import express from 'express';
import SnapAPI from '@sleywill/snapapi-js';

const app = express();
const snap = new SnapAPI(process.env.SNAPAPI_KEY);

app.get('/screenshot', async (req, res) => {
  const { url } = req.query;
  if (!url) return res.status(400).json({ error: 'url required' });
  
  try {
    const result = await snap.screenshot({
      url: decodeURIComponent(url),
      format: 'jpeg',
      full_page: false,
      width: 1200
    });
    // Redirect to screenshot URL or proxy it
    res.redirect(result.url);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000);

Generating PDFs from HTML in Node.js

// Generate a PDF from any URL
const pdf = await snap.pdf({
  url: 'https://example.com/invoice/123',
  format: 'A4',          // A4 | A3 | Letter | Legal
  landscape: false,
  margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' },
  print_background: true
});
console.log(pdf.url); // PDF URL ready to download or store

Batch screenshots with Promise.all

const urls = ['https://site1.com', 'https://site2.com', 'https://site3.com'];

// Parallel (faster, uses more quota simultaneously)
const results = await Promise.all(
  urls.map(url => snap.screenshot({ url, format: 'jpeg' }))
);

// Sequential with rate control (for large batches)
const screenshots = [];
for (const url of urls) {
  screenshots.push(await snap.screenshot({ url, format: 'jpeg' }));
  await new Promise(r => setTimeout(r, 100)); // 100ms between requests
}

Error handling and retries

async function screenshotWithRetry(url, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await snap.screenshot({ url, format: 'jpeg' });
    } catch (err) {
      if (attempt === maxRetries) throw err;
      // Exponential backoff: 1s, 2s, 4s
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
    }
  }
}

OG image generation for Next.js apps

One of the most common Node.js use cases for screenshot APIs is generating Open Graph images for social sharing. Here's how to integrate SnapAPI into a Next.js API route:

// pages/api/og-image.js
import SnapAPI from '@sleywill/snapapi-js';
const snap = new SnapAPI(process.env.SNAPAPI_KEY);

export default async function handler(req, res) {
  const { url } = req.query;
  const screenshot = await snap.screenshot({
    url: decodeURIComponent(url),
    width: 1200,
    height: 630,        // Standard OG image dimensions
    format: 'jpeg',
    full_page: false,
    wait_until: 'networkidle'
  });
  // Cache for 1 hour
  res.setHeader('Cache-Control', 's-maxage=3600');
  res.setHeader('Location', screenshot.url);
  res.status(302).end();
}

Advanced Node.js Patterns

1. Batch Screenshots with Concurrency Control

const axios = require('axios');
const { Semaphore } = require('async-mutex');

const SNAPAPI_KEY = 'YOUR_KEY';
const CONCURRENCY = 5;
const semaphore = new Semaphore(CONCURRENCY);

async function screenshotOne(url) {
  const [value, release] = await semaphore.acquire();
  try {
    const response = await axios.get('https://api.snapapi.pics/v1/screenshot', {
      headers: { 'X-API-Key': SNAPAPI_KEY },
      params: { url, format: 'png', full_page: true, width: 1280 },
      responseType: 'arraybuffer'
    });
    return { url, data: response.data, status: 'ok' };
  } catch (err) {
    return { url, error: err.message, status: 'error' };
  } finally {
    release();
  }
}

async function batchScreenshots(urls) {
  const results = await Promise.all(urls.map(screenshotOne));
  const ok = results.filter(r => r.status === 'ok');
  const failed = results.filter(r => r.status === 'error');
  console.log(`Done: ${ok.length} ok, ${failed.length} failed`);
  return results;
}

// Screenshot 20 pages at max 5 concurrent
const urls = Array.from({ length: 20 }, (_, i) => `https://example.com/page-${i}`);
batchScreenshots(urls);

2. PDF Generation from Express Route

const express = require('express');
const axios = require('axios');
const app = express();

app.get('/invoice/:id/pdf', async (req, res) => {
  const invoiceUrl = `https://yourapp.com/invoice/${req.params.id}?token=${req.query.token}`;

  try {
    const response = await axios.post(
      'https://api.snapapi.pics/v1/pdf',
      { url: invoiceUrl, format: 'A4', print_background: true,
        margin: { top: '20px', right: '20px', bottom: '20px', left: '20px' } },
      { headers: { 'X-API-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
        responseType: 'arraybuffer' }
    );

    res.set({
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="invoice-${req.params.id}.pdf"`,
      'Content-Length': response.data.length
    });
    res.send(Buffer.from(response.data));
  } catch (err) {
    res.status(500).json({ error: 'PDF generation failed', detail: err.message });
  }
});

app.listen(3000);

3. OG Image Generator for Next.js

// pages/api/og.js (Next.js API route)
import axios from 'axios';

export default async function handler(req, res) {
  const { title, slug } = req.query;

  // Check cache first (implement with Redis or filesystem)
  const cacheKey = `og-${slug}`;

  const response = await axios.get('https://api.snapapi.pics/v1/screenshot', {
    headers: { 'X-API-Key': process.env.SNAPAPI_KEY },
    params: {
      url: `https://yoursite.com/og-template?title=${encodeURIComponent(title)}`,
      format: 'png',
      width: 1200,
      height: 630,
      delay: 1000  // Wait for fonts to load
    },
    responseType: 'arraybuffer'
  });

  res.setHeader('Content-Type', 'image/png');
  res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=86400');
  res.send(Buffer.from(response.data));
}

4. Retry with Exponential Backoff

async function screenshotWithRetry(url, maxRetries = 3) {
  const delay = (ms) => new Promise(r => setTimeout(r, ms));

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await axios.get('https://api.snapapi.pics/v1/screenshot', {
        headers: { 'X-API-Key': process.env.SNAPAPI_KEY },
        params: { url, format: 'png', full_page: true },
        responseType: 'arraybuffer',
        timeout: 30000
      });
      return response.data;
    } catch (err) {
      if (attempt === maxRetries) throw err;
      const waitMs = Math.pow(2, attempt) * 1000;  // 1s, 2s, 4s
      console.log(`Attempt ${attempt + 1} failed. Retrying in ${waitMs}ms...`);
      await delay(waitMs);
    }
  }
}

Common Node.js Integration Patterns

Queue with Bull/BullMQ

Add screenshot jobs to a Redis-backed queue. Process with 3-5 workers. Handles spikes and provides retry logic out of the box.

Stream to S3

Pipe the arraybuffer response directly to S3 PutObject. Avoid loading large images into memory. Use presigned URLs for client delivery.

Fastify Plugin

Wrap SnapAPI calls in a Fastify plugin with schema validation. Expose a /screenshot endpoint internally, decorate with the API client instance.

Start Building with SnapAPI in Node.js

200 free calls/month. npm install snapapi-js for a typed SDK. No credit card required.

Start Free Read Docs