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}`;
}