Screenshot API Best Practices: Production-Ready Implementation Guide
Screenshot API Best Practices: Production-Ready Implementation
Building with a screenshot API is straightforward. Building it right for production requires following proven best practices. This comprehensive guide covers everything from basic implementation to advanced production patterns.
Table of Contents
- Authentication & Security
- Error Handling & Retries
- Performance Optimization
- Cost Management
- Monitoring & Observability
- Testing Strategies
- Scaling Patterns
1. Authentication & Security
API Key Management
Never hardcode API keys:
// ❌ Bad - exposed in source code
const API_KEY = 'sk_live_abc123...';
// ✅ Good - environment variables
const API_KEY = process.env.SNAPSHOT_API_KEY;
// ✅ Better - secrets manager
const API_KEY = await getSecret('SNAPSHOT_API_KEY');
Use environment-specific keys:
const config = {
development: process.env.SNAPSHOT_API_KEY_DEV,
staging: process.env.SNAPSHOT_API_KEY_STAGING,
production: process.env.SNAPSHOT_API_KEY_PROD
};
const API_KEY = config[process.env.NODE_ENV];
Server-Side Only
Keep API keys on the server:
// ❌ Bad - client-side exposure
// frontend/app.js
const screenshot = await fetch('https://www.snapshotai.dev/api/v1/screenshots', {
headers: { 'Authorization': `Bearer ${API_KEY}` } // Exposed!
});
// ✅ Good - proxy through your backend
// frontend/app.js
const screenshot = await fetch('/api/screenshot', {
method: 'POST',
body: JSON.stringify({ url: 'https://example.com' })
});
// backend/api/screenshot.js
export async function POST(req) {
const { url } = await req.json();
const response = await fetch('https://www.snapshotai.dev/api/v1/screenshots', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SNAPSHOT_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url })
});
return response;
}
Input Validation
Validate and sanitize URLs:
import { URL } from 'url';
function validateUrl(input) {
try {
const url = new URL(input);
// Allow only HTTPS
if (url.protocol !== 'https:') {
throw new Error('Only HTTPS URLs are allowed');
}
// Block internal/private IPs
const hostname = url.hostname;
if (
hostname === 'localhost' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')
) {
throw new Error('Internal URLs are not allowed');
}
return url.toString();
} catch (error) {
throw new Error(`Invalid URL: ${error.message}`);
}
}
// Usage
const safeUrl = validateUrl(userInput);
const screenshot = await captureScreenshot(safeUrl);
Rate Limiting
Implement client-side rate limiting:
import Bottleneck from 'bottleneck';
// Respect API limits
const limiter = new Bottleneck({
maxConcurrent: 40, // Max concurrent requests
minTime: 25 // Min time between requests (40/second)
});
// Wrap API calls
const captureScreenshot = limiter.wrap(async (url) => {
return await api.capture({ url });
});
// Use normally
const screenshot = await captureScreenshot('https://example.com');
2. Error Handling & Retries
Comprehensive Error Handling
async function captureWithErrorHandling(url) {
try {
const response = await fetch('https://www.snapshotai.dev/api/v1/screenshots', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url })
});
if (!response.ok) {
const error = await response.json();
switch (response.status) {
case 400:
throw new ValidationError(error.message);
case 401:
throw new AuthenticationError('Invalid API key');
case 429:
throw new RateLimitError('Rate limit exceeded', error.retry_after);
case 500:
case 502:
case 503:
throw new ServerError('Service temporarily unavailable');
default:
throw new APIError(`HTTP ${response.status}: ${error.message}`);
}
}
return await response.json();
} catch (error) {
if (error instanceof NetworkError) {
// Network-level error (timeout, DNS, etc.)
throw new APIError('Network error: ' + error.message);
}
throw error;
}
}
Smart Retry Logic
import pRetry from 'p-retry';
async function captureWithRetry(url, options = {}) {
return pRetry(
async () => {
try {
return await captureScreenshot(url, options);
} catch (error) {
// Don't retry validation errors
if (error instanceof ValidationError) {
throw new pRetry.AbortError(error.message);
}
// Don't retry authentication errors
if (error instanceof AuthenticationError) {
throw new pRetry.AbortError(error.message);
}
// Retry rate limit errors with backoff
if (error instanceof RateLimitError) {
await sleep(error.retryAfter * 1000);
throw error;
}
// Retry server errors
if (error instanceof ServerError) {
throw error;
}
// Retry network errors
throw error;
}
},
{
retries: 3,
factor: 2,
minTimeout: 1000,
maxTimeout: 10000,
onFailedAttempt: (error) => {
console.log(
`Attempt ${error.attemptNumber} failed. ${error.retriesLeft} retries left.`
);
}
}
);
}
Timeout Configuration
import AbortController from 'abort-controller';
async function captureWithTimeout(url, timeoutMs = 30000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch('https://www.snapshotai.dev/api/v1/screenshots', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url }),
signal: controller.signal
});
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new TimeoutError(`Request timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timeout);
}
}
3. Performance Optimization
Request Optimization
// ✅ Optimal configuration
const screenshot = await api.capture({
url: 'https://example.com',
// Viewport
viewport_width: 1280, // Not larger than needed
viewport_height: 720,
// Format
format: 'jpeg', // Faster than PNG
image_quality: 85, // Sweet spot
// Performance
block_ads: true, // 30-50% faster
block_trackers: true,
block_cookie_banners: true,
// Timing
wait_for_network_idle: true, // Smart waiting
timeout: 10000, // Fail fast
// Only when needed
full_page: false // Faster than full page
});
Parallel Processing
import pLimit from 'p-limit';
async function captureMultiple(urls) {
const limit = pLimit(40); // Respect rate limits
const results = await Promise.allSettled(
urls.map(url =>
limit(() => captureWithRetry(url))
)
);
const successful = results.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results.filter(r => r.status === 'rejected')
.map(r => ({ error: r.reason }));
return { successful, failed };
}
Caching Strategy
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function captureWithCache(url, options = {}, ttl = 3600) {
const cacheKey = `screenshot:${url}:${JSON.stringify(options)}`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Capture screenshot
const screenshot = await captureScreenshot(url, options);
// Cache result
await redis.setex(cacheKey, ttl, JSON.stringify(screenshot));
return screenshot;
}
4. Cost Management
Usage Tracking
class ScreenshotClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.usage = {
requests: 0,
successful: 0,
failed: 0,
cost: 0
};
}
async capture(url, options = {}) {
this.usage.requests++;
try {
const screenshot = await captureScreenshot(url, options);
this.usage.successful++;
this.usage.cost += this.calculateCost(options);
return screenshot;
} catch (error) {
this.usage.failed++;
throw error;
}
}
calculateCost(options) {
// Basic screenshot
let cost = 0.01;
// Full page costs more
if (options.full_page) cost += 0.005;
// Video recording costs more
if (options.format === 'video') cost += 0.02;
return cost;
}
getUsage() {
return {
...this.usage,
successRate: this.usage.successful / this.usage.requests,
avgCost: this.usage.cost / this.usage.requests
};
}
}
Budget Alerts
class BudgetMonitor {
constructor(monthlyBudget) {
this.monthlyBudget = monthlyBudget;
this.currentSpend = 0;
}
async trackRequest(cost) {
this.currentSpend += cost;
// Alert at 80%
if (this.currentSpend > this.monthlyBudget * 0.8) {
await this.sendAlert(
'WARNING: 80% of monthly budget used',
this.currentSpend,
this.monthlyBudget
);
}
// Block at 100%
if (this.currentSpend > this.monthlyBudget) {
throw new BudgetExceededError(
`Monthly budget of $${this.monthlyBudget} exceeded`
);
}
}
async sendAlert(message, current, budget) {
console.error(message);
// Send email/Slack notification
}
}
Optimization Tips
// Avoid full page when unnecessary
const screenshot = await api.capture({
url,
full_page: false, // Save cost
clip_height: 1200 // Capture what you need
});
// Use templates for repeated configs
const template = await api.createTemplate({
name: 'docs-screenshot',
config: {
viewport_width: 1280,
viewport_height: 720,
block_ads: true
}
});
// Reuse template (faster + cheaper)
const screenshot = await api.captureWithTemplate({
template_id: template.id,
url: url
});
5. Monitoring & Observability
Structured Logging
import winston from 'winston';
const logger = winston.createLogger({
format: winston.format.json(),
transports: [new winston.transports.Console()]
});
async function captureWithLogging(url, options = {}) {
const startTime = Date.now();
const requestId = generateId();
logger.info('Screenshot request started', {
requestId,
url,
options
});
try {
const screenshot = await captureScreenshot(url, options);
logger.info('Screenshot request completed', {
requestId,
url,
duration: Date.now() - startTime,
size: screenshot.size,
status: 'success'
});
return screenshot;
} catch (error) {
logger.error('Screenshot request failed', {
requestId,
url,
duration: Date.now() - startTime,
error: error.message,
stack: error.stack,
status: 'failure'
});
throw error;
}
}
Metrics Tracking
import { Counter, Histogram } from 'prom-client';
const requestCounter = new Counter({
name: 'screenshot_requests_total',
help: 'Total screenshot requests',
labelNames: ['status', 'format']
});
const requestDuration = new Histogram({
name: 'screenshot_duration_seconds',
help: 'Screenshot request duration',
buckets: [0.5, 1, 2, 5, 10]
});
async function captureWithMetrics(url, options = {}) {
const end = requestDuration.startTimer();
try {
const screenshot = await captureScreenshot(url, options);
requestCounter.inc({
status: 'success',
format: options.format || 'png'
});
end();
return screenshot;
} catch (error) {
requestCounter.inc({
status: 'failure',
format: options.format || 'png'
});
end();
throw error;
}
}
Health Checks
export async function GET() {
const checks = {
api: false,
redis: false,
database: false
};
try {
// Check API connectivity
const response = await fetch('https://www.snapshotai.dev/api/v1/health');
checks.api = response.ok;
// Check Redis
await redis.ping();
checks.redis = true;
// Check database
await db.query('SELECT 1');
checks.database = true;
const healthy = Object.values(checks).every(Boolean);
return new Response(JSON.stringify(checks), {
status: healthy ? 200 : 503,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({
...checks,
error: error.message
}), {
status: 503,
headers: { 'Content-Type': 'application/json' }
});
}
}
6. Testing Strategies
Unit Tests
import { describe, it, expect, vi } from 'vitest';
describe('Screenshot Client', () => {
it('should capture screenshot successfully', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ url: 'https://cdn.example.com/screenshot.png' })
});
global.fetch = mockFetch;
const result = await captureScreenshot('https://example.com');
expect(mockFetch).toHaveBeenCalledWith(
'https://www.snapshotai.dev/api/v1/screenshots',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Authorization': expect.stringContaining('Bearer ')
})
})
);
expect(result).toHaveProperty('url');
});
it('should handle API errors', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
status: 400,
json: async () => ({ message: 'Invalid URL' })
});
global.fetch = mockFetch;
await expect(captureScreenshot('invalid-url'))
.rejects
.toThrow('Invalid URL');
});
});
Integration Tests
describe('Screenshot Integration', () => {
it('should capture real screenshot', async () => {
const screenshot = await captureScreenshot('https://example.com', {
viewport_width: 1280,
viewport_height: 720
});
expect(screenshot).toMatchObject({
url: expect.stringMatching(/^https:\/\/cdn\./),
width: 1280,
height: 720,
format: expect.stringMatching(/^(png|jpeg)$/)
});
}, 30000); // 30s timeout for real requests
});
7. Scaling Patterns
Queue-Based Processing
import Bull from 'bull';
const screenshotQueue = new Bull('screenshots', process.env.REDIS_URL);
// Producer
async function queueScreenshot(url, options = {}) {
const job = await screenshotQueue.add({
url,
options
}, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
return job.id;
}
// Consumer
screenshotQueue.process(async (job) => {
const { url, options } = job.data;
const screenshot = await captureScreenshot(url, options);
// Store result
await saveScreenshot(job.id, screenshot);
return screenshot;
});
// Usage
const jobId = await queueScreenshot('https://example.com');
console.log(`Queued job ${jobId}`);
Webhook Integration
// Request with webhook
const screenshot = await api.capture({
url: 'https://example.com',
webhook_url: 'https://myapp.com/webhooks/screenshot'
});
// Webhook handler
export async function POST(req) {
const { screenshot_id, url, status } = await req.json();
if (status === 'completed') {
// Process completed screenshot
await processScreenshot(screenshot_id, url);
} else if (status === 'failed') {
// Handle failure
await handleFailure(screenshot_id);
}
return new Response('OK', { status: 200 });
}
Conclusion
Following these best practices ensures your screenshot implementation is:
- Secure: API keys protected, inputs validated
- Reliable: Error handling, retries, monitoring
- Performant: Optimized configs, caching, parallelization
- Cost-effective: Usage tracking, budget alerts
- Observable: Logging, metrics, health checks
- Testable: Unit and integration tests
- Scalable: Queues, webhooks, distributed processing
Start with the basics and layer in sophistication as you scale.
Questions about implementation? Our team is here to help at support@snapshotai.dev
Continue Reading
AI-Powered Screenshot Automation: The Future of Web Capture
Discover how AI and machine learning are revolutionizing screenshot automation with 99.8% accuracy in content blocking and intelligent detection across 50+ languages.
Screenshot API vs Puppeteer: A Technical Comparison for Production Applications
An in-depth technical analysis comparing screenshot APIs with self-hosted Puppeteer solutions. Learn which approach is right for your production needs.