Screenshot API Node.js Guide 2026 — fetch, axios & Express

Capture screenshots and generate PDFs from Node.js without Puppeteer or Playwright. Full examples for native fetch, axios, Express.js download endpoints, AWS Lambda, and concurrent batch processing.

Get Free API Key

Why Node.js Developers Replace Puppeteer with a Screenshot API

Puppeteer is the first tool most Node.js developers reach for when they need screenshots. It works in local development but introduces a cascade of production problems. The bundled Chromium binary is 300+ MB, which pushes Docker images past acceptable size limits and makes Lambda deployments impossible under the 250 MB unzipped layer limit. Memory consumption of 400 to 600 MB per Chrome instance means a Node.js server handling concurrent screenshot requests needs multiple gigabytes of RAM dedicated purely to browser processes. Crash recovery — Chromium processes that die under heavy load or OOM conditions — requires watchdog scripts and process managers that add operational complexity unrelated to your core application logic. SnapAPI replaces all of that with a single HTTP call: pass the URL, get back the screenshot bytes, no browser management required.

Native fetch Screenshot (Node 18+)

const apiKey = process.env.SNAPAPI_KEY;

async function screenshot(url, options = {}) {
  const params = new URLSearchParams({
    access_key: apiKey,
    url,
    format: options.format || 'png',
    full_page: options.fullPage ? '1' : '0',
    viewport_width: String(options.width || 1280),
    viewport_height: String(options.height || 800),
  });

  const res = await fetch(`https://snapapi.pics/screenshot?${params}`);
  if (!res.ok) {
    const err = await res.text();
    throw new Error(`SnapAPI ${res.status}: ${err}`);
  }
  return res.arrayBuffer();
}

// Save a PNG screenshot
const buffer = await screenshot('https://example.com', { fullPage: true });
const fs = require('fs');
fs.writeFileSync('screenshot.png', Buffer.from(buffer));
console.log('Saved', buffer.byteLength, 'bytes');

// Generate a PDF
const pdf = await screenshot('https://example.com', { format: 'pdf', fullPage: true });
fs.writeFileSync('page.pdf', Buffer.from(pdf));

Axios with Retry Interceptor

const axios = require('axios');

const client = axios.create({
  baseURL: 'https://snapapi.pics',
  timeout: 30_000,
  responseType: 'arraybuffer',
});

// Retry on 429 and 5xx
client.interceptors.response.use(null, async (error) => {
  const config = error.config;
  if (!config || config._retryCount >= 3) return Promise.reject(error);
  const status = error.response?.status;
  if (status !== 429 && status < 500) return Promise.reject(error);
  config._retryCount = (config._retryCount || 0) + 1;
  const delay = Math.pow(2, config._retryCount) * 1000;
  await new Promise(r => setTimeout(r, delay));
  return client(config);
});

async function axiosScreenshot(url) {
  const { data } = await client.get('/screenshot', {
    params: { access_key: process.env.SNAPAPI_KEY, url, format: 'png', full_page: '1' }
  });
  return Buffer.from(data);
}

Express.js Screenshot Download Endpoint

const express = require('express');
const fetch = require('node-fetch');
const app = express();

app.get('/screenshot', async (req, res) => {
  const { url, format = 'png', full_page = '1' } = req.query;
  if (!url) return res.status(400).json({ error: 'url required' });

  const params = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY,
    url, format, full_page,
  });

  try {
    const snap = await fetch('https://snapapi.pics/screenshot?' + params);
    if (!snap.ok) throw new Error('API error ' + snap.status);

    const mimeType = format === 'pdf' ? 'application/pdf' : 'image/png';
    const filename = format === 'pdf' ? 'page.pdf' : 'screenshot.png';
    res.set('Content-Type', mimeType);
    res.set('Content-Disposition', `attachment; filename="${filename}"`);
    snap.body.pipe(res);
  } catch (err) {
    res.status(502).json({ error: err.message });
  }
});

app.listen(3000, () => console.log('Running on port 3000'));

AWS Lambda Screenshot Function

SnapAPI works on AWS Lambda with zero configuration changes. No browser layer, no custom runtime — just your Node.js function making an outbound HTTPS call:

const https = require('https');

exports.handler = async (event) => {
  const { url, format = 'png' } = event.queryStringParameters || {};
  if (!url) return { statusCode: 400, body: 'url required' };

  const params = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY,
    url, format, full_page: '1',
  });

  const data = await new Promise((resolve, reject) => {
    const chunks = [];
    https.get('https://snapapi.pics/screenshot?' + params, (res) => {
      res.on('data', chunk => chunks.push(chunk));
      res.on('end', () => resolve(Buffer.concat(chunks)));
      res.on('error', reject);
    });
  });

  const mimeType = format === 'pdf' ? 'application/pdf' : 'image/png';
  return {
    statusCode: 200,
    headers: { 'Content-Type': mimeType },
    body: data.toString('base64'),
    isBase64Encoded: true,
  };
};

Concurrent Batch Screenshots with p-limit

const pLimit = require('p-limit');
const limit = pLimit(5);  // max 5 concurrent requests

async function batchScreenshots(urls) {
  const tasks = urls.map(url => limit(async () => {
    try {
      const buffer = await axiosScreenshot(url);
      return { url, bytes: buffer.length, ok: true };
    } catch (err) {
      return { url, error: err.message, ok: false };
    }
  }));
  return Promise.all(tasks);
}

const urls = [
  'https://example.com',
  'https://node.js.org',
  'https://npmjs.com',
];

const results = await batchScreenshots(urls);
results.forEach(r => {
  if (r.ok) console.log(`OK  ${r.url} — ${r.bytes} bytes`);
  else console.log(`ERR ${r.url} — ${r.error}`);
});

Uploading Screenshots to S3 from Node.js

For production workflows that archive screenshots, pipe the SnapAPI response directly to S3 using the AWS SDK v3 streaming upload. This avoids loading the entire screenshot into memory before writing to S3, which is important for large full-page screenshots that can be 2 to 5 MB:

const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({ region: process.env.AWS_REGION });

async function captureAndUpload(url, bucket, key) {
  const params = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY,
    url, format: 'png', full_page: '1',
  });
  const snap = await fetch('https://snapapi.pics/screenshot?' + params);
  if (!snap.ok) throw new Error('Screenshot failed: ' + snap.status);
  const buffer = Buffer.from(await snap.arrayBuffer());

  await s3.send(new PutObjectCommand({
    Bucket: bucket,
    Key: key,
    Body: buffer,
    ContentType: 'image/png',
    CacheControl: 'public, max-age=86400',
  }));
  return `https://${bucket}.s3.amazonaws.com/${key}`;
}

TypeScript Integration with Full Type Safety

TypeScript projects benefit from typed wrappers around the SnapAPI HTTP calls. Define an interface for the screenshot options, create a typed client class, and use it throughout your TypeScript codebase with full IntelliSense support:

interface ScreenshotOptions {
  url: string;
  format?: 'png' | 'jpeg' | 'webp' | 'pdf';
  fullPage?: boolean;
  viewportWidth?: number;
  viewportHeight?: number;
  delay?: number;
  waitForSelector?: string;
}

interface ScrapeResult {
  title: string;
  content: string;
  links: string[];
  og_image?: string;
}

class SnapAPIClient {
  constructor(private apiKey: string) {}

  async screenshot(options: ScreenshotOptions): Promise {
    const params = new URLSearchParams({
      access_key: this.apiKey,
      url: options.url,
      format: options.format || 'png',
      full_page: options.fullPage ? '1' : '0',
      viewport_width: String(options.viewportWidth || 1280),
      viewport_height: String(options.viewportHeight || 800),
    });
    if (options.delay) params.set('delay', String(options.delay));
    if (options.waitForSelector) params.set('wait_for_selector', options.waitForSelector);

    const res = await fetch(`https://snapapi.pics/screenshot?${params}`);
    if (!res.ok) throw new Error(`SnapAPI ${res.status}`);
    return Buffer.from(await res.arrayBuffer());
  }

  async scrape(url: string): Promise {
    const params = new URLSearchParams({ access_key: this.apiKey, url, output: 'markdown' });
    const res = await fetch(`https://snapapi.pics/scrape?${params}`);
    if (!res.ok) throw new Error(`SnapAPI scrape ${res.status}`);
    return res.json() as Promise;
  }
}

const client = new SnapAPIClient(process.env.SNAPAPI_KEY!);
const png = await client.screenshot({ url: 'https://example.com', fullPage: true });

Caching Screenshots in Node.js with Redis

For applications that screenshot the same URLs repeatedly — link preview services, social media schedulers, monitoring dashboards — a Redis cache prevents redundant API calls and dramatically reduces latency for cache hits. Store the screenshot bytes in Redis with a TTL matching how often the underlying page changes. Use the ioredis package for async Redis operations in Node.js:

const Redis = require('ioredis');
const crypto = require('crypto');

const redis = new Redis(process.env.REDIS_URL);
const CACHE_TTL = 3600; // 1 hour in seconds

async function cachedScreenshot(url, options = {}) {
  const cacheKey = 'snap:' + crypto.createHash('md5').update(url + JSON.stringify(options)).digest('hex');
  const cached = await redis.getBuffer(cacheKey);
  if (cached) {
    console.log('cache hit:', url);
    return cached;
  }
  const buffer = await screenshot(url, options);
  await redis.set(cacheKey, buffer, 'EX', CACHE_TTL);
  return buffer;
}

Integration with Next.js App Router

Next.js App Router API routes use the Web Fetch API natively, making SnapAPI integration seamless. The route handler streams the response directly to the client without buffering the entire screenshot in server memory:

// app/api/screenshot/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const url = request.nextUrl.searchParams.get('url');
  if (!url) return NextResponse.json({ error: 'url required' }, { status: 400 });

  const params = new URLSearchParams({
    access_key: process.env.SNAPAPI_KEY!,
    url,
    format: 'png',
    full_page: '1',
  });

  const snap = await fetch(`https://snapapi.pics/screenshot?${params}`);
  if (!snap.ok) return NextResponse.json({ error: 'Screenshot failed' }, { status: 502 });

  return new NextResponse(snap.body, {
    headers: {
      'Content-Type': 'image/png',
      'Cache-Control': 'public, max-age=3600',
    },
  });
}

export const runtime = 'edge'; // works on Vercel Edge too

Monitoring Screenshot API Usage from Node.js

Production Node.js applications should track SnapAPI usage to avoid unexpected quota exhaustion and to diagnose latency regressions. Wrap every SnapAPI call with a thin instrumentation layer that records the target URL, response time in milliseconds, HTTP status code, and response size in bytes. Log these fields as JSON to your existing logging pipeline — Winston, Pino, or Bunyan — so they are queryable in your log aggregation system. For metric alerting, emit a counter increment for success and failure outcomes and a histogram observation for response time. Alert on P95 latency exceeding 8 seconds (indicates pages that need a longer delay parameter), error rate above 2 percent of daily requests (indicates quota exhaustion or proxy issues), and total monthly requests approaching your plan's cap (set an alert at 80 percent consumption to prevent mid-month 429 responses). These observability patterns add fewer than 15 lines to your screenshot helper function and provide full visibility into your SnapAPI usage patterns over time.