DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to build a screenshot API in Node.js

How to Build a Screenshot API in Node.js

If multiple services in your stack need screenshots — your dashboard, your monitoring tool, your email previews — the cleanest pattern is a single internal screenshot API that all of them call. One place to manage the API key, one place to add caching, one place to change providers.

Here's how to build it in Node.js in about 50 lines.

Basic Express API

import express from 'express';
import fetch from 'node-fetch';

const app = express();
app.use(express.json());

app.post('/screenshot', async (req, res) => {
  const { url, html, fullPage = true, format = 'png' } = req.body;

  if (!url && !html) {
    return res.status(400).json({ error: 'url or html required' });
  }

  const capture = await fetch('https://pagebolt.dev/api/v1/screenshot', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.PAGEBOLT_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ url, html, fullPage, blockBanners: true, format })
  });

  if (!capture.ok) {
    return res.status(502).json({ error: 'capture failed' });
  }

  const image = Buffer.from(await capture.arrayBuffer());
  res.setHeader('Content-Type', `image/${format}`);
  res.send(image);
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Add simple in-memory caching

Screenshots of the same URL rarely change within a few minutes. Cache by URL to avoid redundant captures:

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({ max: 200, ttl: 5 * 60 * 1000 }); // 5 min TTL

app.post('/screenshot', async (req, res) => {
  const { url, fullPage = true, format = 'png' } = req.body;
  const cacheKey = `${url}:${fullPage}:${format}`;

  if (cache.has(cacheKey)) {
    res.setHeader('Content-Type', `image/${format}`);
    res.setHeader('X-Cache', 'HIT');
    return res.send(cache.get(cacheKey));
  }

  const capture = await fetch('https://pagebolt.dev/api/v1/screenshot', {
    method: 'POST',
    headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ url, fullPage, blockBanners: true, format })
  });

  const image = Buffer.from(await capture.arrayBuffer());
  cache.set(cacheKey, image);

  res.setHeader('Content-Type', `image/${format}`);
  res.setHeader('X-Cache', 'MISS');
  res.send(image);
});
Enter fullscreen mode Exit fullscreen mode

Extend to video

The same wrapper pattern works for narrated video recordings — expose a /video endpoint that your internal services can call without knowing the upstream API details:

app.post('/video', async (req, res) => {
  const { steps, audioGuide, pace = 'slow' } = req.body;

  const capture = await fetch('https://pagebolt.dev/api/v1/video', {
    method: 'POST',
    headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ steps, audioGuide, pace, frame: { enabled: true, style: 'macos' } })
  });

  const video = Buffer.from(await capture.arrayBuffer());
  res.setHeader('Content-Type', 'video/mp4');
  res.send(video);
});
Enter fullscreen mode Exit fullscreen mode

Your CI pipeline, changelog automation, and onboarding video generator all call /video on your internal service — swap the upstream provider without touching any of them.

Add API key auth

If your internal API is exposed beyond localhost, add a simple key check:

app.use((req, res, next) => {
  if (req.headers['x-internal-key'] !== process.env.INTERNAL_API_KEY) {
    return res.status(401).json({ error: 'unauthorized' });
  }
  next();
});
Enter fullscreen mode Exit fullscreen mode

Rotate INTERNAL_API_KEY independently of PAGEBOLT_API_KEY — your internal callers don't need to know the upstream key.


Try it free — 100 requests/month, no credit card. → pagebolt.dev

Top comments (0)