Menu
Back to Blog
1 min read
KI-Entwicklung

API Integration Patterns für Automation

Robuste API Integrationen. Retry Logic, Rate Limiting, Webhooks und Error Handling für zuverlässige Automatisierung.

API IntegrationRetry LogicRate LimitingWebhooksError HandlingREST API
API Integration Patterns für Automation

API Integration Patterns für Automation

Meta-Description: Robuste API Integrationen. Retry Logic, Rate Limiting, Webhooks und Error Handling für zuverlässige Automatisierung.

Keywords: API Integration, Retry Logic, Rate Limiting, Webhooks, Error Handling, REST API, HTTP Client


Einführung

Zuverlässige API-Integrationen sind das Fundament jeder Automation. Retry Logic, Rate Limiting, Circuit Breaker – diese Patterns machen den Unterschied zwischen fragilen und robusten Systemen.


API Client Architecture

┌─────────────────────────────────────────────────────────────┐
│              ROBUST API CLIENT                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Request Flow:                                              │
│                                                             │
│  Application                                                │
│       ↓                                                    │
│  Rate Limiter (Throttle Requests)                          │
│       ↓                                                    │
│  Retry Handler (Exponential Backoff)                       │
│       ↓                                                    │
│  Circuit Breaker (Fail Fast)                               │
│       ↓                                                    │
│  HTTP Client (Axios/Fetch)                                 │
│       ↓                                                    │
│  Response Handler (Parse/Validate)                         │
│       ↓                                                    │
│  Application                                                │
│                                                             │
│  Cross-Cutting Concerns:                                    │
│  ├── Logging                                               │
│  ├── Metrics                                               │
│  └── Caching                                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Base API Client

import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';

interface ApiClientConfig {
  baseURL: string;
  timeout?: number;
  headers?: Record<string, string>;
  retries?: number;
  retryDelay?: number;
  rateLimit?: {
    maxRequests: number;
    windowMs: number;
  };
}

class ApiClient {
  private client: AxiosInstance;
  private config: Required<ApiClientConfig>;
  private requestTimestamps: number[] = [];

  constructor(config: ApiClientConfig) {
    this.config = {
      timeout: 30000,
      headers: {},
      retries: 3,
      retryDelay: 1000,
      rateLimit: { maxRequests: 100, windowMs: 60000 },
      ...config
    };

    this.client = axios.create({
      baseURL: this.config.baseURL,
      timeout: this.config.timeout,
      headers: this.config.headers
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    // Request Interceptor
    this.client.interceptors.request.use(
      async (config) => {
        await this.waitForRateLimit();
        this.recordRequest();

        console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`);
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response Interceptor
    this.client.interceptors.response.use(
      (response) => {
        console.log(`[API] Response: ${response.status}`);
        return response;
      },
      async (error: AxiosError) => {
        const config = error.config as AxiosRequestConfig & { _retryCount?: number };

        if (!config || !this.shouldRetry(error, config._retryCount || 0)) {
          return Promise.reject(this.formatError(error));
        }

        config._retryCount = (config._retryCount || 0) + 1;

        const delay = this.calculateDelay(config._retryCount);
        console.log(`[API] Retry ${config._retryCount}/${this.config.retries} after ${delay}ms`);

        await this.sleep(delay);
        return this.client.request(config);
      }
    );
  }

  private shouldRetry(error: AxiosError, retryCount: number): boolean {
    if (retryCount >= this.config.retries) return false;

    // Retry bei Netzwerkfehlern
    if (!error.response) return true;

    // Retry bei bestimmten Status Codes
    const retryableStatuses = [408, 429, 500, 502, 503, 504];
    return retryableStatuses.includes(error.response.status);
  }

  private calculateDelay(retryCount: number): number {
    // Exponential Backoff mit Jitter
    const baseDelay = this.config.retryDelay;
    const exponentialDelay = baseDelay * Math.pow(2, retryCount - 1);
    const jitter = Math.random() * 1000;
    return exponentialDelay + jitter;
  }

  private async waitForRateLimit(): Promise<void> {
    const now = Date.now();
    const windowStart = now - this.config.rateLimit.windowMs;

    // Alte Timestamps entfernen
    this.requestTimestamps = this.requestTimestamps.filter(t => t > windowStart);

    if (this.requestTimestamps.length >= this.config.rateLimit.maxRequests) {
      const oldestRequest = this.requestTimestamps[0];
      const waitTime = oldestRequest + this.config.rateLimit.windowMs - now;

      if (waitTime > 0) {
        console.log(`[API] Rate limit reached, waiting ${waitTime}ms`);
        await this.sleep(waitTime);
      }
    }
  }

  private recordRequest() {
    this.requestTimestamps.push(Date.now());
  }

  private formatError(error: AxiosError): Error {
    if (error.response) {
      return new Error(
        `API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`
      );
    }
    if (error.request) {
      return new Error(`Network Error: ${error.message}`);
    }
    return error;
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // Public Methods
  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.get<T>(url, config);
    return response.data;
  }

  async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.post<T>(url, data, config);
    return response.data;
  }

  async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.put<T>(url, data, config);
    return response.data;
  }

  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response = await this.client.delete<T>(url, config);
    return response.data;
  }
}

Circuit Breaker Pattern

enum CircuitState {
  CLOSED = 'CLOSED',     // Normal Operation
  OPEN = 'OPEN',         // Failing, block requests
  HALF_OPEN = 'HALF_OPEN' // Testing recovery
}

interface CircuitBreakerConfig {
  failureThreshold: number;
  successThreshold: number;
  timeout: number;
}

class CircuitBreaker {
  private state: CircuitState = CircuitState.CLOSED;
  private failureCount = 0;
  private successCount = 0;
  private lastFailureTime?: number;
  private config: CircuitBreakerConfig;

  constructor(config: Partial<CircuitBreakerConfig> = {}) {
    this.config = {
      failureThreshold: 5,
      successThreshold: 2,
      timeout: 30000,
      ...config
    };
  }

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      if (this.shouldAttemptReset()) {
        this.state = CircuitState.HALF_OPEN;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private shouldAttemptReset(): boolean {
    return (
      this.lastFailureTime !== undefined &&
      Date.now() - this.lastFailureTime >= this.config.timeout
    );
  }

  private onSuccess() {
    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++;

      if (this.successCount >= this.config.successThreshold) {
        this.reset();
      }
    }

    this.failureCount = 0;
  }

  private onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.state === CircuitState.HALF_OPEN) {
      this.state = CircuitState.OPEN;
      this.successCount = 0;
    } else if (this.failureCount >= this.config.failureThreshold) {
      this.state = CircuitState.OPEN;
    }
  }

  private reset() {
    this.state = CircuitState.CLOSED;
    this.failureCount = 0;
    this.successCount = 0;
    this.lastFailureTime = undefined;
  }

  getState(): CircuitState {
    return this.state;
  }
}

// Verwendung
const circuitBreaker = new CircuitBreaker({ failureThreshold: 3 });

async function callExternalApi() {
  return circuitBreaker.execute(async () => {
    return apiClient.get('/data');
  });
}

Webhook Handler

import { createHmac, timingSafeEqual } from 'crypto';

interface WebhookConfig {
  secret: string;
  tolerance: number;  // Sekunden
}

class WebhookHandler {
  private config: WebhookConfig;
  private handlers: Map<string, (data: any) => Promise<void>> = new Map();

  constructor(config: WebhookConfig) {
    this.config = config;
  }

  register(event: string, handler: (data: any) => Promise<void>) {
    this.handlers.set(event, handler);
  }

  async handle(
    payload: string,
    signature: string,
    timestamp: string
  ): Promise<void> {
    // 1. Signature validieren
    if (!this.verifySignature(payload, signature, timestamp)) {
      throw new Error('Invalid webhook signature');
    }

    // 2. Timestamp validieren (Replay Attack Prevention)
    if (!this.verifyTimestamp(timestamp)) {
      throw new Error('Webhook timestamp too old');
    }

    // 3. Event verarbeiten
    const event = JSON.parse(payload);
    const handler = this.handlers.get(event.type);

    if (handler) {
      await handler(event.data);
    } else {
      console.warn(`No handler for event type: ${event.type}`);
    }
  }

  private verifySignature(
    payload: string,
    signature: string,
    timestamp: string
  ): boolean {
    const signedPayload = `${timestamp}.${payload}`;
    const expectedSignature = createHmac('sha256', this.config.secret)
      .update(signedPayload)
      .digest('hex');

    const sigBuffer = Buffer.from(signature);
    const expectedBuffer = Buffer.from(`sha256=${expectedSignature}`);

    return sigBuffer.length === expectedBuffer.length &&
           timingSafeEqual(sigBuffer, expectedBuffer);
  }

  private verifyTimestamp(timestamp: string): boolean {
    const webhookTime = parseInt(timestamp, 10) * 1000;
    const now = Date.now();
    const tolerance = this.config.tolerance * 1000;

    return Math.abs(now - webhookTime) <= tolerance;
  }
}

// Express Route
import express from 'express';

const app = express();
const webhookHandler = new WebhookHandler({
  secret: process.env.WEBHOOK_SECRET!,
  tolerance: 300  // 5 Minuten
});

// Webhook Events registrieren
webhookHandler.register('order.created', async (data) => {
  await processNewOrder(data);
});

webhookHandler.register('payment.completed', async (data) => {
  await completePayment(data);
});

// Webhook Endpoint
app.post('/webhooks',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.headers['x-webhook-signature'] as string;
    const timestamp = req.headers['x-webhook-timestamp'] as string;

    try {
      await webhookHandler.handle(
        req.body.toString(),
        signature,
        timestamp
      );

      res.status(200).send('OK');
    } catch (error) {
      console.error('Webhook error:', error);
      res.status(400).send('Invalid webhook');
    }
  }
);

Pagination Handler

interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    pageSize: number;
    total: number;
    hasMore: boolean;
  };
}

async function* paginatedFetch<T>(
  apiClient: ApiClient,
  endpoint: string,
  pageSize: number = 100
): AsyncGenerator<T[], void, unknown> {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await apiClient.get<PaginatedResponse<T>>(
      `${endpoint}?page=${page}&pageSize=${pageSize}`
    );

    yield response.data;

    hasMore = response.pagination.hasMore;
    page++;
  }
}

// Cursor-based Pagination
async function* cursorPaginatedFetch<T>(
  apiClient: ApiClient,
  endpoint: string,
  limit: number = 100
): AsyncGenerator<T[], void, unknown> {
  let cursor: string | null = null;

  while (true) {
    const url = cursor
      ? `${endpoint}?cursor=${cursor}&limit=${limit}`
      : `${endpoint}?limit=${limit}`;

    const response = await apiClient.get<{
      data: T[];
      nextCursor: string | null;
    }>(url);

    yield response.data;

    if (!response.nextCursor) break;
    cursor = response.nextCursor;
  }
}

// Verwendung
async function fetchAllProducts() {
  const allProducts: Product[] = [];

  for await (const products of paginatedFetch<Product>(apiClient, '/products')) {
    allProducts.push(...products);
    console.log(`Fetched ${allProducts.length} products so far`);
  }

  return allProducts;
}

Response Caching

interface CacheEntry<T> {
  data: T;
  expiresAt: number;
  etag?: string;
}

class ResponseCache {
  private cache: Map<string, CacheEntry<any>> = new Map();
  private defaultTTL: number;

  constructor(defaultTTL: number = 60000) {
    this.defaultTTL = defaultTTL;
  }

  set<T>(key: string, data: T, ttl?: number, etag?: string) {
    this.cache.set(key, {
      data,
      expiresAt: Date.now() + (ttl || this.defaultTTL),
      etag
    });
  }

  get<T>(key: string): CacheEntry<T> | null {
    const entry = this.cache.get(key);

    if (!entry) return null;

    if (Date.now() > entry.expiresAt) {
      this.cache.delete(key);
      return null;
    }

    return entry;
  }

  invalidate(pattern: string | RegExp) {
    for (const key of this.cache.keys()) {
      if (typeof pattern === 'string' ? key.includes(pattern) : pattern.test(key)) {
        this.cache.delete(key);
      }
    }
  }
}

// Mit ETag Support
async function fetchWithCache<T>(
  apiClient: ApiClient,
  url: string,
  cache: ResponseCache
): Promise<T> {
  const cacheKey = url;
  const cached = cache.get<T>(cacheKey);

  const headers: Record<string, string> = {};

  if (cached?.etag) {
    headers['If-None-Match'] = cached.etag;
  }

  try {
    const response = await apiClient.get<T>(url, { headers });
    const etag = (response as any).headers?.etag;

    cache.set(cacheKey, response, undefined, etag);
    return response;
  } catch (error: any) {
    if (error.response?.status === 304 && cached) {
      return cached.data;
    }
    throw error;
  }
}

Fazit

Robuste API Integrationen brauchen:

  1. Retry Logic: Exponential Backoff mit Jitter
  2. Rate Limiting: Client-seitige Throttling
  3. Circuit Breaker: Fail Fast bei Ausfällen
  4. Caching: Reduziert Last und Latenz

Production-ready von Anfang an.


Bildprompts

  1. "API calls with retry arrows showing resilience, error recovery"
  2. "Circuit breaker switch between services, fail fast concept"
  3. "Rate limiter queue with request tickets, throttling visualization"

Quellen