Express.js Screenshot API Integration
Build a screenshot service with Express and ScreenshotAPI. Production-ready Node.js examples with caching, error handling, and middleware.
Last updated: 2026-03-25
Try ScreenshotAPI free
5 free credits. No credit card required.
Build a Screenshot Service with Express and ScreenshotAPI
Express.js is the most widely used Node.js web framework, and adding screenshot functionality is a common requirement for SaaS dashboards, CMS tools, and SEO platforms. The typical approach involves running Puppeteer alongside Express, but that introduces a 170 MB Chromium dependency, heavy RAM usage, and browser process management that complicates deployments.
An Express screenshot API integration with ScreenshotAPI removes that complexity. Your Express server makes one HTTP request per screenshot and receives image bytes in return. No headless browser, no binary management, no out-of-memory crashes.
Quick Start
- Sign up for ScreenshotAPI to get your API key. You receive 5 free credits on signup.
- Install the JavaScript SDK.
- Create an Express route that proxies screenshot requests.
Installation
bashnpm install express screenshotapi
Set the environment variable:
bashexport SCREENSHOTAPI_KEY=sk_live_xxxxx
Basic Example
A minimal Express server that serves screenshots:
typescriptimport express from 'express' const app = express() const API_BASE = 'https://screenshotapi.to/api/v1/screenshot' app.get('/api/screenshot', async (req, res) => { const { url, width = '1440', height = '900', type = 'png' } = req.query if (!url || typeof url !== 'string') { return res.status(400).json({ error: 'url parameter is required' }) } try { const params = new URLSearchParams({ url, width: String(width), height: String(height), type: String(type), }) const response = await fetch(`${API_BASE}?${params}`, { headers: { 'x-api-key': process.env.SCREENSHOTAPI_KEY! }, }) if (!response.ok) { return res.status(response.status).json({ error: 'Capture failed' }) } const buffer = Buffer.from(await response.arrayBuffer()) res.set('Content-Type', `image/${type}`) res.set('Cache-Control', 'public, max-age=86400') res.send(buffer) } catch (error) { res.status(500).json({ error: 'Internal server error' }) } }) app.listen(3000, () => console.log('Screenshot server running on :3000'))
Test it:
bashcurl "http://localhost:3000/api/screenshot?url=https://example.com" --output shot.png
Express Screenshot Service Module
Extract screenshot logic into a reusable module:
typescript// services/screenshot.ts interface ScreenshotOptions { url: string width?: number height?: number type?: 'png' | 'jpeg' | 'webp' quality?: number fullPage?: boolean colorScheme?: 'light' | 'dark' waitUntil?: 'networkidle' | 'load' | 'domcontentloaded' delay?: number } const API_BASE = 'https://screenshotapi.to/api/v1/screenshot' export async function captureScreenshot( options: ScreenshotOptions, retries = 2 ): Promise<Buffer> { const params = new URLSearchParams({ url: options.url, width: String(options.width ?? 1440), height: String(options.height ?? 900), type: options.type ?? 'png', waitUntil: options.waitUntil ?? 'networkidle', }) if (options.quality) params.set('quality', String(options.quality)) if (options.fullPage) params.set('fullPage', 'true') if (options.colorScheme) params.set('colorScheme', options.colorScheme) if (options.delay) params.set('delay', String(options.delay)) for (let attempt = 0; attempt <= retries; attempt++) { try { const res = await fetch(`${API_BASE}?${params}`, { headers: { 'x-api-key': process.env.SCREENSHOTAPI_KEY! }, signal: AbortSignal.timeout(30_000), }) if (!res.ok) throw new Error(`API returned ${res.status}`) return Buffer.from(await res.arrayBuffer()) } catch (error) { if (attempt === retries) throw error await new Promise((r) => setTimeout(r, 1000 * (attempt + 1))) } } throw new Error('Screenshot capture failed after retries') }
Express Router with Middleware
Organize routes with Express Router and add middleware:
typescript// routes/screenshot.ts import { Router } from 'express' import { captureScreenshot } from '../services/screenshot' const router = Router() router.get('/', async (req, res) => { const { url, width, height, type = 'webp', quality, fullPage } = req.query if (!url || typeof url !== 'string') { return res.status(400).json({ error: 'url parameter is required' }) } try { const image = await captureScreenshot({ url, width: width ? Number(width) : 1440, height: height ? Number(height) : 900, type: type as 'png' | 'jpeg' | 'webp', quality: quality ? Number(quality) : undefined, fullPage: fullPage === 'true', }) res.set('Content-Type', `image/${type}`) res.set('Cache-Control', 'public, max-age=3600, s-maxage=86400') res.send(image) } catch (error) { res.status(502).json({ error: 'Screenshot capture failed' }) } }) router.get('/download', async (req, res) => { const url = req.query.url as string if (!url) return res.status(400).json({ error: 'url is required' }) try { const image = await captureScreenshot({ url, type: 'png' }) res.set('Content-Disposition', 'attachment; filename="screenshot.png"') res.set('Content-Type', 'image/png') res.send(image) } catch (error) { res.status(502).json({ error: 'Download failed' }) } }) export default router
Mount it:
typescriptimport screenshotRouter from './routes/screenshot' app.use('/api/screenshot', screenshotRouter)
Streaming Responses
For large full-page screenshots, stream the response instead of buffering it:
typescriptapp.get('/api/screenshot/stream', async (req, res) => { const url = req.query.url as string if (!url) return res.status(400).json({ error: 'url is required' }) const params = new URLSearchParams({ url, width: '1440', height: '900', type: 'png', fullPage: 'true', }) const response = await fetch(`${API_BASE}?${params}`, { headers: { 'x-api-key': process.env.SCREENSHOTAPI_KEY! }, }) if (!response.ok || !response.body) { return res.status(502).json({ error: 'Capture failed' }) } res.set('Content-Type', 'image/png') res.set('Cache-Control', 'public, max-age=3600') const reader = response.body.getReader() const push = async () => { const { done, value } = await reader.read() if (done) { res.end() return } res.write(value) await push() } await push() })
Caching with Node-Cache
Store recent screenshots in memory to reduce API calls:
typescriptimport NodeCache from 'node-cache' const cache = new NodeCache({ stdTTL: 3600, maxKeys: 500 }) app.get('/api/screenshot/cached', async (req, res) => { const url = req.query.url as string if (!url) return res.status(400).json({ error: 'url is required' }) const cacheKey = `screenshot:${url}` const cached = cache.get<Buffer>(cacheKey) if (cached) { res.set('Content-Type', 'image/webp') res.set('X-Cache', 'HIT') return res.send(cached) } try { const image = await captureScreenshot({ url, type: 'webp', quality: 80 }) cache.set(cacheKey, image) res.set('Content-Type', 'image/webp') res.set('X-Cache', 'MISS') res.send(image) } catch (error) { res.status(502).json({ error: 'Capture failed' }) } })
Production Tips
Rate Limiting
Protect your endpoint with express-rate-limit:
typescriptimport rateLimit from 'express-rate-limit' const screenshotLimiter = rateLimit({ windowMs: 60 * 1000, max: 20, message: { error: 'Too many screenshot requests' }, }) app.use('/api/screenshot', screenshotLimiter)
Input Validation
Validate URLs before forwarding them:
typescriptfunction isValidUrl(input: string): boolean { try { const url = new URL(input) return url.protocol === 'http:' || url.protocol === 'https:' } catch { return false } }
Error Monitoring
Log screenshot failures for debugging:
typescriptapp.use('/api/screenshot', (err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error(`Screenshot error for ${req.query.url}:`, err.message) res.status(500).json({ error: 'Internal server error' }) })
Scaling
Express servers calling ScreenshotAPI scale horizontally without any special configuration. Since the screenshot rendering happens off-server, you can run multiple Express instances behind a load balancer without worrying about Chromium process limits. Check the pricing page for volume credit packages.
Further Reading
- How to Take Screenshots with JavaScript covers the fundamentals of programmatic screenshot capture.
- The JavaScript SDK documentation has the complete parameter reference.
- See the AWS Lambda integration for serverless deployments.
Frequently asked questions
Why use ScreenshotAPI instead of Puppeteer with Express?
Puppeteer bundles a 170 MB Chromium binary, consumes 200+ MB of RAM per browser instance, and crashes under load. ScreenshotAPI offloads rendering entirely, keeping your Express server lightweight and stable.
Can I use ScreenshotAPI with Express middleware?
Yes. The API call is a standard fetch request that works inside any Express route handler or middleware. You can add authentication, rate limiting, and caching middleware around it.
How do I stream screenshots to the client?
Fetch the response from ScreenshotAPI and pipe the readable stream directly to the Express response object using response.body.pipe(res).
Does ScreenshotAPI support WebP format?
Yes. Pass type=webp in the query parameters. You can also set quality=80 for a good balance between file size and visual quality.
Related resources
Start capturing screenshots today
Create a free account and get 5 credits to try the API. No credit card required. Pay only for what you use.