Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

In a Next.js App Router project, create a server-side Route Handler that launches Puppeteer, navigates to a validated URL, captures the page, and returns the image bytes. The endpoint needs a Node.js runtime and a deployment that includes a compatible browser executable; a static export cannot perform request-time browser captures.

1. Add a screenshot Route Handler

Put this file at app/api/screenshot/route.ts. It accepts a URL in the query string and returns a full-page PNG. This example is an implementation pattern; import details and browser launch configuration can vary with the Puppeteer version and deployment.

// app/api/screenshot/route.ts
import puppeteer from 'puppeteer';

export const runtime = 'nodejs';

export async function GET(request: Request) {
  const target = new URL(request.url).searchParams.get('url');
  if (!target) {
    return new Response('Missing url', { status: 400 });
  }

  // Validate and restrict target before navigating. See security guidance below.
  let parsed: URL;
  try {
    parsed = new URL(target);
  } catch {
    return new Response('Invalid url', { status: 400 });
  }
  if (!['http:', 'https:'].includes(parsed.protocol)) {
    return new Response('Only HTTP and HTTPS URLs are allowed', { status: 400 });
  }

  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    page.setDefaultNavigationTimeout(30_000);
    await page.goto(parsed.href, { waitUntil: 'networkidle2' });
    const image = await page.screenshot({ type: 'png', fullPage: true });
    return new Response(image, {
      headers: {
        'Content-Type': 'image/png',
        'Cache-Control': 'no-store',
      },
    });
  } catch (error) {
    console.error('Screenshot capture failed', error);
    return new Response('Screenshot capture failed', { status: 502 });
  } finally {
    await browser.close();
  }
}

Call it with a URL-encoded target, for example /api/screenshot?url=https%3A%2F%2Fexample.com. The response is binary PNG data, so a browser can display it directly or a client can save it. Use Cache-Control: no-store when each request must trigger a fresh capture; Next.js Route Handlers are not cached by default, but make any intended caching behavior deliberate.

Protect the endpoint before exposing it

A URL-taking screenshot endpoint can be abused to make your server request internal services. Allow only expected domains where possible; reject loopback, private, link-local, and other internal destinations, and account for DNS resolution and redirects rather than relying only on a string check. Also add authentication or rate limiting if appropriate, cap request and navigation duration, and limit concurrent browser work. Next.js’s self-hosting guide recommends a reverse proxy for protections such as rate limiting, payload limits, and handling slow or malformed requests: Next.js self-hosting guide.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

2. Choose the right readiness condition

The example uses Puppeteer’s networkidle2 condition, which waits for a period with no more than two network connections. That can be a useful starting point, not a universal signal that a page is visually ready: analytics, polling, and other long-lived requests can delay it, while client-rendered content, fonts, animations, or lazy images may still change after network activity settles.

For pages you control, a more deterministic approach is to render a clear ready marker and wait for it before taking the screenshot:

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option
await page.goto(parsed.href, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-screenshot-ready="true"]', {
  timeout: 10_000,
});
const image = await page.screenshot({ type: 'png', fullPage: true });

Choose the condition according to the target site. A selector is meaningful only if the page sets it when the content you need is ready. For third-party pages, combine an appropriate navigation condition with a bounded delay or a known selector when necessary, and treat timeouts as normal failure cases.

3. Capture a viewport, full page, or one element

Puppeteer’s Page.screenshot() captures the current page. To capture just one component, find it and call ElementHandle.screenshot(); Puppeteer scrolls the element into view if needed and captures it through the page screenshot mechanism.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const card = await page.$('.product-card');
if (!card) {
  return new Response('Element not found', { status: 404 });
}
const image = await card.screenshot({ type: 'png' });
Capture choice How to request it Useful for
Viewport page.screenshot({ type: 'png' }) A visible-screen preview at the page’s current viewport size.
Full page page.screenshot({ type: 'png', fullPage: true }) A complete document capture; output can be much taller and larger than a viewport image.
Element element.screenshot({ type: 'png' }) A specific card, chart, or other DOM component.
Clipped region page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 800, height: 600 } }) A defined rectangular region of the page.

Screenshot options also include output format and transparency. Puppeteer returns screenshot bytes as a Uint8Array by default; request base64 output only when the consuming interface needs a base64 string. omitBackground: true can produce a transparent background when used with a compatible output format. See the ScreenshotOptions reference and Page.screenshot() API.

4. Install Puppeteer and its browser

The puppeteer package normally downloads a compatible Chrome for Testing and chrome-headless-shell during installation. If a package manager blocks install scripts, the download may be skipped; Puppeteer’s documented browser-install command is npx puppeteer browsers install. Check the Puppeteer installation guide for the current command and configuration options.

Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Package Browser management Choose it when
puppeteer Downloads a compatible browser as part of installation unless scripts or configuration prevent it. You want Puppeteer to manage a matching browser and your build can include it.
puppeteer-core Does not download Chrome; you supply a managed browser executable or connect to a remote browser. You operate browser infrastructure separately or use a remote browser.

The installation guide lists approximate Chrome for Testing download sizes of 170 MB on macOS, 282 MB on Linux, and 280 MB on Windows. These are platform-specific browser download figures, not the finished application or container size. Confirm that your deployment build can package the browser and that the runtime can execute it.

5. Deploy on a server-capable Next.js runtime

Next.js supports Node.js server and Docker deployments with full framework functionality; static export has limited support. A screenshot endpoint must run server-side on each request and launch or reach a browser, so a static-only export is not sufficient. Review Next.js deployment options for the currently documented deployment modes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Provider constraints are not interchangeable. Before choosing a host, verify its current browser-executable support, packaged artifact size, request duration, memory, and writable-filesystem behavior. Next.js’s general deployment guidance does not set universal function limits. Depending on the host’s lifecycle model and load, you may also evaluate reusing a browser process rather than launching one for every request; manage page cleanup, concurrency, and recovery if you do.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

6. Troubleshoot common failures

Symptom Likely cause What to check or change
Could not find Chrome or executable missing Install scripts were blocked, the browser was not packaged, or the runtime path differs. Run Puppeteer’s browser-install command during build, confirm the browser is included in the deployed artifact, or configure the executable/remote connection when using puppeteer-core.
Launch fails in production although it works locally The deployment runtime may not support the downloaded browser or its required execution environment. Check the host’s current browser, binary, and runtime restrictions; use a compatible Node.js or Docker deployment.
Navigation times out The page remains active, is slow, or never reaches the selected network condition. Set a bounded navigation timeout, choose a condition suited to the page, or wait for a page-specific readiness signal. Do not remove limits on a public endpoint.
Screenshot is blank or missing content Capture happened before client rendering, fonts, or images settled, or the selector did not match. Wait for a meaningful ready selector, verify the element exists, and test the target with the same viewport and browser configuration.
Route returns HTML error instead of an image The handler encountered an exception or returned an error status before capture completed. Check server logs, input validation, browser launch, and navigation; preserve a non-2xx status for failures rather than labeling error text as PNG.
Deployment exceeds size, memory, or execution limits The browser and screenshot workload do not fit the selected host’s constraints. Check current provider limits and consider Docker or a separately managed/remote browser; reduce capture dimensions or concurrency where appropriate.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. Its API returns a screenshot or PDF from one GET request; cookie and consent banners, newsletter popups, and chat widgets are removed before capture, and bot checks, blank pages, failed loads, and cache hits are not billed. Its MCP server gives AI agents screenshot tools, including take_screenshot, get_page_info, and capture_pdf.

For an image response, use cURL:

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

Or make the same request from Python or Node.js:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for request parameters and response details. ScreenshotNeo includes 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000. Sign up for the free plan.

Frequently Asked Questions

Can I use the Edge Runtime for this Route Handler?

No. The example explicitly selects Next.js’s Node.js runtime because it launches Puppeteer and a browser executable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does this endpoint save screenshots for later?

No. It returns the capture in the HTTP response. Add storage separately if you need persistent files or shareable links.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.