Introduktion
API’er fejler. Netværk er upålidelige, afhængigheder går ned, og uventet belastning sker. Forskellen mellem en god API og en fremragende API er, hvordan den håndterer disse fejl.
Denne guide gennemgår praktiske mønstre, jeg har brugt til at bygge robuste API’er i produktion.
Circuit breaker-mønstret
Problemet
Når en efterfølgende service fejler, medfører fortsatte kald:
- Ressourcespild
- Øget latens
- Kan forårsage kaskadefejl
Løsningen
En circuit breaker overvåger fejl og “åbner”, når en grænseværdi nås, og returnerer straks fejl i stedet for at kalde den fejlende service.
class CircuitBreaker {
private state: 'closed' | 'open' | 'half-open' = 'closed';
private failureCount = 0;
private lastFailureTime?: number;
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (this.shouldAttemptReset()) {
this.state = '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 onSuccess() {
this.failureCount = 0;
this.state = 'closed';
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= FAILURE_THRESHOLD) {
this.state = 'open';
}
}
private shouldAttemptReset(): boolean {
return Date.now() - this.lastFailureTime! > RESET_TIMEOUT;
}
}
Reel effekt
Efter implementering af circuit breakers til vores integrationer med betalingsudbydere:
- 90 % reduktion i kaskadefejl
- Forbedrede API-svartider under udfald hos udbydere
- Bedre indsigt i afhængighedernes sundhedstilstand
Genforsøg med eksponentiel backoff
Problemet
Forbigående fejl er almindelige (netværksudsving, midlertidig overbelastning). Øjeblikkelige genforsøg kan forværre situationen.
Løsningen
Genforsøg med eksponentielt stigende forsinkelser plus jitter for at undgå en “thundering herd”-effekt.
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < maxRetries - 1) {
const delay = Math.min(
1000 * Math.pow(2, attempt) + Math.random() * 1000,
10000
);
await sleep(delay);
}
}
}
throw lastError!;
}
Hvornår man skal forsøge igen
Ikke alle fejl bør medføre genforsøg:
- ✅ Netværkstimeouts
- ✅ 503 Service Unavailable
- ✅ 429 Too Many Requests
- ❌ 400 Bad Request
- ❌ 401 Unauthorized
- ❌ 404 Not Found
Timeouts
Problemet
Uden timeouts kan en langsom afhængighed blokere hele din API.
Løsningen
Sæt aggressive timeouts, og fejl hurtigt.
async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), timeoutMs);
});
return Promise.race([promise, timeout]);
}
Valg af timeout-værdier
- P95-latens + buffer: Hvis P95 er 200 ms, sæt timeout til 500 ms
- Overvej kaskaderende timeouts: Hvert lag bør have en kortere timeout end laget over
- Overvåg og justér: Brug målinger til at finjustere timeout-værdierne
Graceful degradation
Problemet
Når en ikke-kritisk afhængighed fejler, skal hele din API så fejle?
Løsningen
Identificér kritiske vs. ikke-kritiske afhængigheder, og degradér elegant.
async function getUserProfile(userId: string) {
const [user, preferences, recommendations] = await Promise.allSettled([
fetchUser(userId), // Kritisk
fetchPreferences(userId), // Ikke-kritisk
fetchRecommendations(userId) // Ikke-kritisk
]);
if (user.status === 'rejected') {
throw new Error('Failed to fetch user');
}
return {
user: user.value,
preferences: preferences.status === 'fulfilled'
? preferences.value
: null,
recommendations: recommendations.status === 'fulfilled'
? recommendations.value
: []
};
}
Rate limiting
Problemet
Ubegrænsede forespørgsler kan overvælde din API og efterfølgende services.
Løsningen
Implementér rate limiting på flere niveauer:
- Grænser pr. bruger: Forhindr individuelle brugere i at overvælde systemet
- Globale grænser: Beskyt den samlede systemkapacitet
- Afhængighedsgrænser: Respektér grænser hos efterfølgende services
class RateLimiter {
private requests = new Map<string, number[]>();
async checkLimit(key: string, limit: number, windowMs: number): Promise<boolean> {
const now = Date.now();
const windowStart = now - windowMs;
const requests = this.requests.get(key) || [];
const recentRequests = requests.filter(time => time > windowStart);
if (recentRequests.length >= limit) {
return false;
}
recentRequests.push(now);
this.requests.set(key, recentRequests);
return true;
}
}
Overvågning og observability
Væsentlige målinger
Følg disse målinger for hvert API-endpoint:
- Forespørgselsrate
- Fejlrate
- Latens (P50, P95, P99)
- Afhængighedernes sundhedstilstand
Struktureret logging
Log nok kontekst til at kunne fejlsøge problemer:
logger.info('Payment processed', {
userId,
paymentId,
amount,
provider,
duration: Date.now() - startTime,
success: true
});
Konklusion
At bygge robuste API’er kræver, at man tænker over fejltilstande på forhånd. Mønstrene gennemgået her - circuit breakers, genforsøg, timeouts, graceful degradation og rate limiting - udgør et solidt fundament.
Husk: fejl vil ske. Målet er at håndtere dem elegant og bevare en god brugeroplevelse, selv når noget går galt.
Videre læsning
- Release It! af Michael Nygard
- Site Reliability Engineering af Google
- Designing Data-Intensive Applications af Martin Kleppmann