# 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

```javascript
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);
```

## Add simple in-memory caching

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

```javascript
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);
});
```

## 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:

```javascript
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);
});
```

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:

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

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](https://pagebolt.dev)
