Skip to main content
Screenshot API Best Practices: Production-Ready Implementation Guide - Technical tutorial
Back to Blog
Technical

Screenshot API Best Practices: Production-Ready Implementation Guide

SnapshotAI Team
Author
October 20, 2024
15 min read

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

  1. Authentication & Security
  2. Error Handling & Retries
  3. Performance Optimization
  4. Cost Management
  5. Monitoring & Observability
  6. Testing Strategies
  7. 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

SnapshotAI Team
Technical writer and developer advocate at SnapshotAI. Passionate about making developer tools accessible and performant.
SnapshotAI - Reliable Screenshot API for Developers