Get started with 33% off your first certification using code: 33OFFNEW

How to turn any URL into a screenshot from Node.js

4 min read
Published on 30th June 2026

You need a screenshot of a web page from a Node script: a thumbnail for a link, a visual record of a competitor's pricing page, an automated grab of a dashboard for a report. The instinct is to reach for Puppeteer, which means bundling a copy of Chromium, keeping it patched, and finding somewhere with enough memory to run it. For most jobs that is far more than you need. A screenshot API does the browser part for you, and your Node code shrinks to a single fetch. This article shows how to turn any URL into a PNG from Node, including full-page captures and grabbing one specific element.

One request instead of a browser

The whole task comes down to sending a URL to an endpoint and getting an image back. No puppeteer.launch, no --no-sandbox flags, no Chromium layer in your deploy. We use the HTML to Image API from plain Node, with the global fetch that ships in Node 18 and later.

async function screenshot(url, opts = {}) {
  const res = await fetch('https://app.html2img.com/api/screenshot', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.HTML2IMG_API_KEY,
    },
    body: JSON.stringify({ url, width: 1280, height: 800, ...opts }),
  });

  if (!res.ok) {
    throw new Error(`Screenshot failed: ${res.status} ${await res.text()}`);
  }

  const { url: imageUrl } = await res.json();
  return imageUrl;
}

Call it with any page and you get back a hosted PNG URL.

const image = await screenshot('https://example.com');
console.log(image); // https://i.html2img.com/...

Keep your key in an environment variable, never in the source. The width and height set the viewport the page is captured at, just like a browser window.

Saving the file to disk

A hosted URL is handy, but often you want the bytes locally to attach to an email or commit to a report folder. Fetch the image and write it out.

import { writeFile } from 'node:fs/promises';

async function saveScreenshot(url, path) {
  const imageUrl = await screenshot(url);
  const bytes = Buffer.from(await (await fetch(imageUrl)).arrayBuffer());
  await writeFile(path, bytes);
  return path;
}

await saveScreenshot('https://example.com', './example.png');

That is the entire round trip: request the screenshot, download the result, write the file.

Capturing the full page

By default you get the viewport. To capture the entire scrollable page, top to bottom, set full_page. This is the one you want for archiving a long article or a pricing page that runs well below the fold.

const longPage = await screenshot('https://example.com/pricing', {
  full_page: true,
});

The full_page flag and the rest of the capture options are listed in the parameters reference, so you can see exactly what each one does.

Grabbing one element instead of the whole page

Sometimes you do not want the page, you want a piece of it: a chart, a card, a single widget. Pass a selector and the API captures just that element, cropped to its bounds. It waits for the element to be present before shooting.

const chart = await screenshot('https://example.com/dashboard', {
  selector: '#revenue-chart',
});

This is far cleaner than capturing the whole page and cropping in code, because the crop tracks the element's real size rather than fixed pixel coordinates that break the moment the layout shifts.

Screenshotting a list of URLs

Reporting jobs usually mean several pages at once. Map over the list, but cap how many run at the same time so you are polite to the API and do not blow your rate limit. A small concurrency helper does the job without a dependency.

async function mapLimit(items, limit, fn) {
  const results = [];
  const queue = [...items.entries()];

  async function worker() {
    for (const [i, item] of queue.splice(0)) {
      results[i] = await fn(item);
    }
  }

  await Promise.all(Array.from({ length: limit }, worker));
  return results;
}

const pages = [
  'https://example.com',
  'https://example.com/about',
  'https://example.com/pricing',
];

const shots = await mapLimit(pages, 3, (url) => screenshot(url));

For a long-running batch, prefer a webhook over waiting on each response: the API can call you back when each image is ready, which keeps your script from blocking on slow renders. The webhook option is documented alongside the other parameters.

What you have now

A screenshot function that turns any URL into a PNG, options for full-page and single-element captures, a save-to-disk helper and a batch runner with sane concurrency. All of it is plain Node and one fetch, with no browser to install, patch or babysit. When the task is "give me an image of this page", that is usually all it needs to be.