DEV Community

Moonlit Capy
Moonlit Capy

Posted on • Originally published at blog.elunari.uk

API Integration Patterns for Production Systems

10 min read

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))
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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" }),
})
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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.


Explore 85+ free developer tools or support this work.

Top comments (0)