Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
16 KiB
16 KiB
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:
- Retry Logic: Exponential Backoff mit Jitter
- Rate Limiting: Client-seitige Throttling
- Circuit Breaker: Fail Fast bei Ausfällen
- Caching: Reduziert Last und Latenz
Production-ready von Anfang an.
Bildprompts
- "API calls with retry arrows showing resilience, error recovery"
- "Circuit breaker switch between services, fail fast concept"
- "Rate limiter queue with request tickets, throttling visualization"