Building API integrations that work in production — not just in Postman — requires patterns for handling the real world: flaky networks, rate limits, schema changes, and partial failures.
Retry with Exponential Backoff
Network errors and 5xx responses are inevitable. A retry strategy with exponential backoff prevents overwhelming a recovering service while maximizing your chances of success.
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(url, options)
if (res.ok) return res
if (res.status < 500) throw new Error(`Client error: ${res.status}`)
} catch (err) {
if (attempt === maxRetries) throw err
}
const delay = Math.min(1000 * 2 ** attempt, 30000)
const jitter = delay * (0.5 + Math.random() * 0.5)
await new Promise((r) => setTimeout(r, jitter))
}
}Circuit Breaker
When an upstream service is down, continuing to send requests wastes resources and slows down your entire system. A circuit breaker short-circuits failed requests after a threshold.
class CircuitBreaker {
failures = 0; lastFailure = 0; state = "closed"
constructor(private threshold = 5, private resetMs = 60000) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === "open") {
if (Date.now() - this.lastFailure > this.resetMs)
this.state = "half-open"
else throw new Error("Circuit is open")
}
try {
const result = await fn()
this.failures = 0; this.state = "closed"
return result
} catch (err) {
this.failures++; this.lastFailure = Date.now()
if (this.failures >= this.threshold) this.state = "open"
throw err
}
}
}Idempotency Keys
For mutating operations (payments, order creation), an idempotency key ensures that retried requests don't duplicate side effects — the server reuses the original response.
const idempotencyKey = crypto.randomUUID()
const response = await fetch("/api/payments", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ amount: 4999, currency: "USD" }),
})Rate Limiting (Client Side)
Respect the API's rate limits before they enforce them. A token bucket algorithm gives you smooth, predictable request pacing.
class RateLimiter {
private tokens: number
private lastRefill: number
constructor(
private maxTokens: number,
private refillRate: number // tokens per second
) {
this.tokens = maxTokens
this.lastRefill = Date.now()
}
async acquire(): Promise<void> {
this.refill()
if (this.tokens < 1) {
const waitMs = (1 / this.refillRate) * 1000
await new Promise((r) => setTimeout(r, waitMs))
this.refill()
}
this.tokens--
}
private refill() {
const now = Date.now()
const elapsed = (now - this.lastRefill) / 1000
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate)
this.lastRefill = now
}
}Wrapping Up
These patterns aren't optional for production systems. Retries handle transient failures, circuit breakers prevent cascade failures, idempotency keys prevent duplicates, and rate limiting keeps you within bounds. Layer them together for integrations that survive the real world.