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.
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
- Store API keys in environment variables, never in source code
- Use different keys for development, staging, and production
- Rotate keys regularly (every 90 days is a good cadence)
- Use server-side requests only -- never expose keys in client-side JavaScript
Do Not
- Commit API keys to version control (even private repos)
- Share keys via Slack, email, or chat
- Use the same key across all environments
- Make API calls from the browser where keys are visible
// 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.
- Read the rate limit headers -- most APIs return X-RateLimit-Remaining and X-RateLimit-Reset
- Implement client-side throttling -- queue requests and space them out
- Use batch endpoints -- our batch API captures multiple URLs in one request
- Process async -- use webhooks for non-blocking workflows
5. Caching: Avoid Redundant Calls
If you request the same data repeatedly, cache the response to reduce API calls and improve performance.
- HTTP caching -- respect Cache-Control and ETag headers
- Application caching -- store responses in Redis or in-memory cache
- CDN caching -- for screenshot images, cache on a CDN like Cloudflare
- Set appropriate TTL -- screenshots of static pages can be cached for hours; dynamic pages need shorter TTL
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:
- Log all API calls -- request URL, response status, response time
- Track error rates -- alert when errors exceed a threshold
- Monitor latency -- detect performance degradation early
- Track usage -- monitor API consumption against your plan limits
- Set up health checks -- periodically verify the API is responding
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.
- Fallback images -- show a placeholder when screenshot capture fails
- Cached responses -- serve stale data when fresh data is unavailable
- Feature flags -- disable API-dependent features without crashing
- Circuit breakers -- stop calling a failing API after repeated failures
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
- API keys stored in environment variables
- Error handling for all HTTP status codes
- Retry logic with exponential backoff
- Client-side rate limiting
- Response caching where appropriate
- Request timeouts configured
- Logging and monitoring in place
- Input validation before API calls
- Graceful degradation for API failures
- Using official SDK when available
Get Started with ScreenshotAPI
100 free screenshots per month. Full API with SDKs, webhooks, and batch processing.