ScreenshotNeo

BlogHow-to

Puppeteer Screenshot in an Express API: Return an Image Response

Capture a page with Puppeteer and return its screenshot as a correctly typed image response from Express, with runnable code and production guidance.

By the ScreenshotNeo team4 October 20268 min read

To return a Puppeteer screenshot from an Express API, call page.screenshot(), set the response Content-Type to match the image format, and send the screenshot bytes as a Buffer. Puppeteer returns a Uint8Array by default; Express can send a Buffer as binary data.

1. Create an Express screenshot endpoint

This minimal ES module example launches Chromium for a request, navigates to a fixed URL, captures a PNG, and returns it directly. Replace the target URL with the page your endpoint should capture.

import express from 'express';
import puppeteer from 'puppeteer';

const app = express();

app.get('/screenshot', async (req, res, next) => {
  let browser;
  try {
    browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'networkidle2' });

    const image = await page.screenshot({ type: 'png' });
    res.type('png').send(Buffer.from(image));
  } catch (error) {
    next(error);
  } finally {
    await browser?.close();
  }
});

app.listen(3000, () => {
  console.log('Listening on http://localhost:3000');
});

Install express and puppeteer, save this as an ES module (for example, server.mjs), then run it with Node.js. Puppeteer’s installation and browser setup depend on your runtime environment; follow its installation guide. Open http://localhost:3000/screenshot in a browser or save the response with a client such as curl.

The finally block closes the browser after success or failure. This per-request launch is easy to understand, but may be expensive at higher traffic. See the lifecycle section before using it as a production architecture.

2. Why the response headers matter

A screenshot is binary data. Set its media type before sending it so browsers, proxies, and clients can interpret it correctly. If you send a Buffer without setting the type, Express uses application/octet-stream. For PNG, use image/png; for JPEG, use image/jpeg. The response header must agree with the format Puppeteer generated.

res.type('png') resolves the extension to the PNG media type. You can instead use res.set('Content-Type', 'image/png'). Express response API documents Buffer responses and content types.

3. Choose the screenshot options

Puppeteer’s ScreenshotOptions control the image output and captured area. The useful options for an Express image endpoint include:

Option Effect When to use it
type Chooses png or jpeg; PNG is the default. Match the required output and response MIME type.
quality Sets lossy image quality for JPEG; it does not apply to PNG. Reduce JPEG size when some compression loss is acceptable.
fullPage Captures the full scrollable page instead of just the viewport; default is false. Return a full-page document preview. Long pages can produce large images.
clip Captures a specified rectangle. Return a particular region. Do not combine with fullPage.
omitBackground Omits the default background to allow transparency in supported output. Use with PNG when a transparent background is needed.
encoding Defaults to binary output; base64 is an alternative. Keep binary output for an HTTP image response. Base64 is usually more suitable for embedding in text formats.
path Writes the screenshot to a file instead of requiring only in-memory handling. Use when you need a generated file for later serving or processing.

Example: capture a full-page JPEG and return a matching content type.

const image = await page.screenshot({
  type: 'jpeg',
  quality: 82,
  fullPage: true
});
res.type('jpeg').send(Buffer.from(image));

For a viewport PNG, the default is sufficient:

const image = await page.screenshot();
res.type('png').send(Buffer.from(image));

Check the Puppeteer options reference for the exact options supported by the version you install.

4. Wait for the page you need to capture

The example uses waitUntil: 'networkidle2', which Puppeteer’s guide demonstrates. It is not a universal readiness rule. Some pages keep network requests open for analytics or live updates; other pages finish navigation before client-side content is rendered.

Choose a navigation completion condition that fits the target, then wait for a page-specific element when the screenshot depends on rendered content:

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-screenshot-ready]', { timeout: 10000 });
const image = await page.screenshot({ type: 'png' });

Use a selector that indicates the content is actually ready, rather than relying on an arbitrary delay. If the target has no reliable readiness marker, a short explicit wait can help, but it increases response time and may still be too short or unnecessarily long.

5. Return the bytes or serve a saved file

For a screenshot generated during the request, sending its bytes with res.send() avoids a disk round trip. Convert Puppeteer’s Uint8Array result with Buffer.from(image) to make the binary response explicit.

If you save the screenshot to disk, Express can send it with res.sendFile(). Construct the path from trusted values or constrain it with a fixed root; never let an untrusted request path select arbitrary files. For a known directory of static image assets, use express.static(). See the sendFile and static files documentation.

6. Handle browser lifecycle and request failures

The sample uses a browser per request for clarity and closes it in finally, including when navigation or capture throws. At higher request volumes, launching a browser for every request can add overhead. A service may instead reuse a managed browser process while creating an isolated page or context per request. That design needs careful cleanup and concurrency limits; the right configuration depends on the workload and runtime.

Forward unexpected failures to Express error middleware with next(error). Do not try to send a second response if headers or body have already been sent. Express exposes res.headersSent for checking response state. A simple error handler can return a generic server error to the client and log details on the server:

app.use((error, req, res, next) => {
  console.error(error);
  if (res.headersSent) return next(error);
  res.status(500).json({ error: 'Screenshot capture failed' });
});

In a real API, also define request timeouts and a policy for browser or page cleanup if a client disconnects. Do not expose internal exception messages or browser details to callers.

7. Keep the endpoint bounded and reliable

The example captures a fixed URL. If callers can submit a URL, validate it and restrict which destinations the server can reach. Otherwise, the endpoint can be abused to make the server request internal or otherwise unintended addresses. Apply authentication and rate limits appropriate to your API, cap navigation and selector waits, and limit concurrent captures to the resources available in your deployment.

Full-page captures and pages with heavy assets can use more memory and produce much larger responses than viewport screenshots. Prefer the smallest capture area and image format that meets the consumer’s needs. Set an explicit maximum request duration and return a clear failure status if navigation or capture exceeds it. These are deployment safeguards; the API references do not prescribe universal limits or workload-specific performance figures.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its one-call endpoint returns an image or PDF, so your Express service does not have to launch and manage Puppeteer for this capture:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp

See the ScreenshotNeo API documentation for request options and setup. Cookie banners are accepted like a visitor and removed, along with 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, with no card required.

9. Troubleshooting

Symptom Likely cause Fix
The client downloads an unknown file or does not display the image. The response has the generic application/octet-stream type or the wrong image type. Set res.type('png') or the matching type before sending the Buffer.
The image is corrupt or appears empty. Text or an incomplete response was sent, or the page had not rendered the intended content. Send the screenshot bytes directly; verify readiness with a page-specific selector and check navigation errors.
Navigation hangs or times out. The page keeps connections active, or the chosen readiness condition is unsuitable. Choose an appropriate waitUntil setting and wait for the content you need. Bound the wait with a timeout.
Browser processes accumulate after errors. Cleanup is missing on an error path. Close the browser in finally, and ensure reused pages or contexts are also released.
Requests are slow under load. Launching a browser per request adds startup work, or too many captures compete for resources. Consider a managed browser process, isolated pages or contexts, and bounded concurrency. Measure in your own deployment; no universal performance figures apply.
The response fails after headers have been sent. Error handling attempted to send a second response. Check res.headersSent and delegate the error instead of writing another body.
Full-page images are too large. The page is long or has large visual content. Use viewport capture or a clip, and consider JPEG quality when transparency is not needed.

10. Performance, reliability, and cost

Capturing a page requires browser work, network navigation, rendering, and image encoding. A per-request browser launch is straightforward but can be costly at higher request volumes. Reusing a browser process may reduce repeated startup work, but requires isolation, cleanup, and concurrency management. The cited documentation provides no benchmark that predicts performance for a particular application.

For reliability, choose a readiness condition tied to the page, set time limits, handle failures through Express middleware, and release browser resources on every path. For cost, account for the compute and memory required to run browser processes and the target pages; there is no universal per-screenshot cost because deployment and page complexity vary. If you need a managed screenshot endpoint instead, ScreenshotNeo lists plan prices and billing behavior in its documentation.

11. FAQ

Can I display the API response directly in an HTML image?

Yes. Return the image bytes with the correct image content type, then point an <img> element at the endpoint. Protect the endpoint if captures should not be public.

Should I return PNG or JPEG?

Use PNG for the default lossless output or when transparency matters. Use JPEG with a quality setting when a lossy image is acceptable. Always set the response type to match.

Can the endpoint capture only part of a page?

Yes. Use Puppeteer’s clip option for a rectangular region, or use a page element workflow if the desired area is tied to an element. Consult the installed Puppeteer version’s API reference for supported options.

Does the code work unchanged in every Express and Puppeteer version?

The example uses current Puppeteer screenshot behavior and the Express 4.x response API reference. Check the documentation for the versions in your project, especially when adapting module syntax, runtime dependencies, or process management.

Sources