DEVELOPER GUIDE

API Integration Best Practices

A practical guide to integrating third-party APIs correctly -- authentication, error handling, rate limiting, caching, and monitoring. Real examples using the ScreenshotAPI.

March 21, 202612 min read

Most API integrations start simple -- make a request, get a response, done. But production integrations need to handle failures, respect rate limits, cache responses, and monitor health. Here are the best practices that separate hobby projects from production-grade integrations.

1. Authentication: Secure Your API Keys

API keys are credentials. Treat them like passwords.

Do

Do Not

// Good: API key from environment variable
const apiKey = process.env.SCREENSHOT_API_KEY;

const response = await fetch(
  "https://screenshotapi-api-production.up.railway.app/v1/screenshot?url=https://example.com",
  {
    headers: {
      Authorization: `Bearer ${apiKey}`
    }
  }
);

2. Error Handling: Expect Failures

Every API call can fail. Network issues, server errors, rate limits, invalid inputs -- your code must handle all of these gracefully. See our detailed guide on screenshot API error handling.

Handle HTTP Status Codes

async function captureScreenshot(url) {
  const response = await fetch(`${API_URL}/v1/screenshot?url=${encodeURIComponent(url)}`, {
    headers: { Authorization: `Bearer ${API_KEY}` }
  });

  switch (response.status) {
    case 200:
      return await response.arrayBuffer(); // Success
    case 400:
      throw new Error("Invalid request: check your parameters");
    case 401:
      throw new Error("Invalid API key");
    case 403:
      throw new Error("Usage limit exceeded");
    case 429:
      // Rate limited -- wait and retry
      const retryAfter = response.headers.get("Retry-After") || 5;
      await sleep(retryAfter * 1000);
      return captureScreenshot(url); // Retry
    case 500:
      throw new Error("Server error -- try again later");
    default:
      throw new Error(`Unexpected status: ${response.status}`);
  }
}

3. Retry Logic: Exponential Backoff

Transient failures (network glitches, server restarts) resolve themselves. Implement retry logic with exponential backoff to handle them automatically.

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (response.ok) return response;

      // Only retry on server errors and rate limits
      if (response.status >= 500 || response.status === 429) {
        if (attempt < maxRetries) {
          const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
          const jitter = Math.random() * 1000;
          await new Promise(r => setTimeout(r, delay + jitter));
          continue;
        }
      }

      throw new Error(`HTTP ${response.status}`);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
    }
  }
}

Key principles: only retry on retryable errors (5xx, 429, network), use exponential delays (1s, 2s, 4s), add random jitter to avoid thundering herd, and set a maximum retry count.

4. Rate Limiting: Respect the Limits

Every API has rate limits. Exceeding them results in 429 errors and potentially temporary bans. Our API documentation specifies 30 requests per minute for screenshots.

5. Caching: Avoid Redundant Calls

If you request the same data repeatedly, cache the response to reduce API calls and improve performance.

6. Timeouts: Set Them Always

Never make an API call without a timeout. A hanging request can block your entire application.

// Set a 30-second timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);

try {
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${API_KEY}` },
    signal: controller.signal
  });
  return await response.arrayBuffer();
} catch (error) {
  if (error.name === "AbortError") {
    console.error("Request timed out after 30 seconds");
  }
  throw error;
} finally {
  clearTimeout(timeout);
}

7. Monitoring and Logging

You cannot fix what you cannot see. Monitor your API integrations:

8. Input Validation

Validate inputs before sending them to the API. This prevents unnecessary API calls and produces clearer error messages for users.

function validateScreenshotRequest(params) {
  if (!params.url) throw new Error("URL is required");

  try {
    new URL(params.url);
  } catch {
    throw new Error("Invalid URL format");
  }

  if (params.width && (params.width < 100 || params.width > 3840)) {
    throw new Error("Width must be between 100 and 3840");
  }

  if (params.format && !["png", "jpeg", "webp"].includes(params.format)) {
    throw new Error("Format must be png, jpeg, or webp");
  }

  return true;
}

9. Graceful Degradation

When an API is down, your application should still work -- just with reduced functionality.

10. SDK vs Direct API Calls

When available, use the official SDK. SDKs handle authentication, retries, error parsing, and type safety for you. ScreenshotAPI provides SDKs for Node.js and Python.

// Using the ScreenshotAPI Node.js SDK
const ScreenshotAPI = require("screenshotapi-node");

const client = new ScreenshotAPI(process.env.SCREENSHOT_API_KEY);

// The SDK handles auth, retries, and error parsing
const screenshot = await client.screenshot("https://example.com", {
  format: "png",
  width: 1280,
  fullPage: true
});

Checklist: Production-Ready API Integration

Get Started with ScreenshotAPI

100 free screenshots per month. Full API with SDKs, webhooks, and batch processing.

Related Articles