Build a Screenshot Service with Puppeteer (Step-by-Step Guide)
In this tutorial, you'll learn about Build a Screenshot Service with Puppeteer (Step. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Build a headless browser screenshot service using Node.js and Puppeteer that captures full-page screenshots of any URL with configurable viewport size, delay timing, and automatic file output.
What You'll Build
You'll build a REST API that accepts a URL and returns a full-page screenshot. Users can customize the viewport width and height, set a delay before capture (for JavaScript-rendered content), choose between PNG and JPEG format, and receive the image file in response.
Why a Screenshot Service Matters
Screenshot services are used everywhere: generating preview images for link sharing (like Slack or Twitter cards), monitoring website visual changes over time, capturing error states for bug reports, and archiving web pages. Security teams use them to inspect live pages for phishing or defacement. The same headless browser technology can automate login flows — a technique used in Durga Antivirus Pro for scanning suspicious URLs in a sandboxed environment.
Prerequisites
- Node.js 18+ installed
- Basic JavaScript and Express.js knowledge
Step 1: Project Setup
mkdir screenshot-service
cd screenshot-service
npm init -y
npm install <a href="/backend/nodejs/">Express</a> puppeteer uuid
Project structure:
screenshot-service/
├── server.js # Express + Puppeteer route
└── screenshots/ # Output directory for captured images
Puppeteer downloads Chromium (~300MB) on first install. If you're on a server with limited space, use PUPPETEER_SKIP_DOWNLOAD=true and point to an existing Chrome installation with executablePath.
Step 2: The Screenshot Endpoint
// server.js
const express = require("express");
const puppeteer = require("puppeteer");
const { v4: uuidv4 } = require("uuid");
const path = require("path");
const fs = require("fs");
const app = express();
app.use(express.json());
const OUTPUT_DIR = path.join(__dirname, "screenshots");
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
app.post("/screenshot", async (req, res) => {
const { url, width = 1280, height = 720, delay = 0, format = "png", fullPage = true } = req.body;
if (!url) return res.status(400).json({ error: "url is required" });
let browser;
try {
browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
await page.setViewport({ width, height });
console.log(`Navigating to ${url}`);
await page.goto(url, { waitUntil: "networkidle2", timeout: 30000 });
if (delay > 0) {
console.log(`Waiting ${delay}ms for content to render`);
await new Promise((r) => setTimeout(r, delay));
}
const filename = `${uuidv4()}.${format}`;
const filepath = path.join(OUTPUT_DIR, filename);
await page.screenshot({ path: filepath, fullPage, type: format });
res.download(filepath, filename, () => {
fs.unlinkSync(filepath);
});
} catch (err) {
res.status(500).json({ error: err.message });
} finally {
if (browser) await browser.close();
}
});
app.listen(3000, () => console.log("Screenshot service on http://localhost:3000"));
Expected output:
curl -X POST http://localhost:3000/screenshot \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","width":1920,"height":1080,"format":"png"}' \
--output example-screenshot.png
This downloads a 1920x1080 full-page screenshot of example.com as example-screenshot.png.
Step 3: Batch Screenshot Endpoint
// Add to server.js
app.post("/screenshot/batch", async (req, res) => {
const { urls, width, height, delay, format } = req.body;
if (!urls || !Array.isArray(urls)) {
return res.status(400).json({ error: "urls must be an array" });
}
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const results = [];
for (const url of urls) {
try {
const page = await browser.newPage();
await page.setViewport({ width: width || 1280, height: height || 720 });
await page.goto(url, { waitUntil: "networkidle2", timeout: 30000 });
if (delay) await new Promise((r) => setTimeout(r, delay));
const filename = `${Date.now()}-${uuidv4().slice(0, 8)}.${format || "png"}`;
const filepath = path.join(OUTPUT_DIR, filename);
await page.screenshot({ path: filepath, fullPage: true });
results.push({ url, filename, status: "ok" });
await page.close();
} catch (err) {
results.push({ url, error: err.message, status: "failed" });
}
}
await browser.close();
res.json({ results });
});
Expected output:
curl -X POST http://localhost:3000/screenshot/batch \
-H "Content-Type: application/json" \
-d '{"urls":["https://example.com","https://httpbin.org"],"format":"jpeg"}'
Response:
{
"results": [
{ "url": "https://example.com", "filename": "1712345678-abc123.jpeg", "status": "ok" },
{ "url": "https://httpbin.org", "filename": "1712345679-def456.jpeg", "status": "ok" }
]
}
Architecture
sequenceDiagram
participant Client
participant Express as Express Server
participant Puppeteer
participant Chromium
Client->>Express: POST /screenshot { url, width, height, delay }
Express->>Puppeteer: puppeteer.launch()
Puppeteer->>Chromium: Start headless browser
Express->>Puppeteer: page.goto(url)
Puppeteer->>Chromium: Navigate to URL
Chromium-->>Puppeteer: Page loaded (networkidle2)
Express->>Puppeteer: Wait delay ms
Express->>Puppeteer: page.screenshot()
Puppeteer-->>Express: PNG buffer
Express-->>Client: Download response
Express->>Puppeteer: browser.close()
Common Errors
1. Error: Failed to launch the browser process
Chromium is missing system dependencies. Install required libraries: sudo apt install -y ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libcups2 libdrm2 libgbm1 libnss3 libxcomposite1 libxdamage1 libxrandr2 xdg-utils.
2. Timeout navigating to page
The page may be slow, or networkidle2 never fires (common with infinite polling apps). Set a fallback timeout: await page.goto(url, { timeout: 15000, waitUntil: "domcontentloaded" }).
3. Screenshot is blank or white
JavaScript-heavy sites need time to render. Increase the delay parameter to 2000-3000ms. Alternatively, wait for a specific selector: await page.waitForSelector("body.loaded").
4. Memory grows with batch requests
Each browser launch is expensive. Reuse a single browser instance across requests using a Connection Pool pattern. Puppeteer's browser.createIncognitoBrowserContext() gives isolated sessions per request.
5. Sandbox errors on Docker
Running Puppeteer inside Docker requires --no-sandbox flag. For better security, run the container with --cap-add=SYS_ADMIN or use puppeteer-extra-plugin-stealth.
Practice Questions
1. What does headless: "new" mean in Puppeteer?
It uses the new headless mode introduced in Chrome 112+, which is closer to the full browser experience than the old headless mode. It supports extensions, PDF printing, and has a smaller performance gap.
2. Why use networkidle2 as the wait condition?
It waits until there are no more than 2 network connections for at least 500ms. This ensures the page and its resources (images, fonts, API calls) are fully loaded before capturing.
3. How would you add authentication for the screenshot endpoint?
Add middleware that checks an API key header: app.use("/screenshot", (req, res, next) => { if (req.headers["x-api-key"] !== process.env.API_KEY) return res.status(401).json({ error: "Unauthorized" }); next(); }).
4. Challenge: Element-specific screenshot
Add a selector parameter. If provided, use page.waitForSelector(selector) and page.$(selector) to capture only that element instead of the full page. Useful for capturing specific widgets or charts.
5. Challenge: Scheduled monitoring
Write a script that calls /screenshot/batch every hour with a list of monitored URLs, saves screenshots with timestamps, and alerts if a page fails to load.
FAQ
Next Steps
- Add image optimization with sharp to compress screenshots before serving
- Explore Puppeteer advanced patterns — request interception, ad blocking, API mocking
- Try building the Link Preview Service project for another URL-processing use case
- Learn about Docker deployment to containerize the service for production
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro