Error-Mapping und Retry
Ein direktes fetch-Call, das JSON ins Template wirft, ist kein API-Layer – es ist ein Wartungsproblem. DTOs, strukturiertes Error-Mapping und Retry-Logik machen die Vue REST API-Integration zu einer stabilen Schicht, auf die man sich verlassen kann.
Inhaltsverzeichnis
- 1. Warum ein strukturierter Vue REST API-Layer unverzichtbar ist
- 2. DTO-Denken: Typensicherheit zwischen API und UI
- 3. Axios-Client konfigurieren: Interceptors und Basis-Setup
- 4. Repository-Pattern: API-Calls kapseln und testbar machen
- 5. Error-Mapping: von HTTP-Fehlern zu App-Fehlern
- 6. Retry-Strategien: Exponential Backoff und idempotente Requests
- 7. useApi-Composable: Ladezustand und Fehler reaktiv verwalten
- 8. Optimistische UI ohne GraphQL: manueller State-Rollback
- 9. HTTP-Client-Optionen im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein strukturierter Vue REST API-Layer unverzichtbar ist
Der häufigste Fehler in wachsenden Vue REST API-Projekten ist das direkte Aufrufen von API-Endpunkten aus Komponenten heraus. Eine Komponente, die fetch('/api/products') direkt im onMounted-Hook aufruft, vermischt Darstellungslogik mit Datenbeschaffungslogik. Das macht die Komponente schwerer testbar, die URL nicht zentral verwaltbar und das Fehlerhandling inkonsistent. Ändert sich die API-Struktur, müssen alle Komponenten, die direkte API-Calls machen, einzeln angepasst werden.
Ein dedizierter API-Layer in einem Vue REST API-Projekt ist keine Overengineering-Maßnahme, sondern eine Investition in Wartbarkeit. Der API-Layer übernimmt: URL-Verwaltung, Authentication-Token-Handling, Request-Serialisierung, Response-Deserialisierung, DTO-Mapping, Error-Normalisierung und Retry-Logik. Die Komponente kennt nur das Composable, das Composable kennt nur das Repository, das Repository kennt nur den HTTP-Client. Diese Schichtenarchitektur macht jeden Layer unabhängig testbar und austauschbar.
Die Investition in einen sauberen Vue REST API-Layer zahlt sich besonders dann aus, wenn die API sich ändert. Backend-Teams ändern Feldnamen, fügen Versionen hinzu oder ändern Response-Strukturen. Mit einem zentralisierten API-Layer mit DTOs ist eine solche Änderung eine einzige Anpassung im DTO-Mapper. Ohne API-Layer ist es ein Refactoring-Marathon durch alle Komponenten, die diese Daten verwenden. Die Erfahrung zeigt: API-Strukturen ändern sich häufiger als erwartet, besonders in der frühen Projektphase.
2. DTO-Denken: Typensicherheit zwischen API und UI
Ein Data Transfer Object (DTO) ist ein einfaches Typen-Objekt, das die Struktur der API-Antwort exakt beschreibt. In einer Vue REST API-Architektur gibt es zwei Arten von Typen: API-DTOs beschreiben, was der Server zurückgibt – mit snake_case-Feldnamen, ISO-Datumsstrings und Backend-spezifischen IDs. Domain-Modelle beschreiben, was die Vue-Anwendung intern verwendet – mit camelCase-Feldnamen, Date-Objekten und Frontend-freundlichen Strukturen. Zwischen beiden steht ein Mapper, der die Transformation übernimmt.
Das DTO-Denken in Vue REST API-Projekten verhindert, dass Backend-Entscheidungen direkt ins Frontend durchdringen. Wenn das Backend ein Datum als Unix-Timestamp liefert und das Frontend ein formatiertes Datumsstring in der UI anzeigen will, liegt diese Transformation im Mapper – nicht in der Komponente, nicht in einer Utility-Funktion, die überall kopiert wird. Wenn das Backend eine Paginierungsstruktur mit current_page, per_page und total liefert und das Frontend mit einem Pagination-Interface arbeitet, ist der Mapper der einzige Ort, der diesen Unterschied kennt.
// src/api/dto/product.dto.ts
// DTO types reflect the exact API response structure (snake_case, raw values)
export interface ProductApiDto {
id: number
sku: string
product_name: string
price_amount: number
price_currency: string
category_id: number
created_at: string // ISO 8601 string from API
is_active: boolean
stock_quantity: number
thumbnail_url: string | null
}
// src/domain/product.model.ts
// Domain model — frontend-friendly structure (camelCase, typed values)
export interface Product {
id: number
sku: string
name: string
price: { amount: number; currency: string }
categoryId: number
createdAt: Date // Parsed Date object
isActive: boolean
stockQuantity: number
thumbnailUrl: string | null
isInStock: boolean // Computed field — not in API response
}
// src/api/mappers/product.mapper.ts
// Mapper: transforms API DTO to domain model — single source of transformation
export function mapProductDto(dto: ProductApiDto): Product {
return {
id: dto.id,
sku: dto.sku,
name: dto.product_name,
price: { amount: dto.price_amount / 100, currency: dto.price_currency },
categoryId: dto.category_id,
createdAt: new Date(dto.created_at),
isActive: dto.is_active,
stockQuantity: dto.stock_quantity,
thumbnailUrl: dto.thumbnail_url,
// Computed field derived from DTO data
isInStock: dto.stock_quantity > 0,
}
}
3. Axios-Client konfigurieren: Interceptors und Basis-Setup
Axios ist der etablierte HTTP-Client für Vue REST API-Projekte, weil er im Gegensatz zur nativen Fetch-API Request- und Response-Interceptors unterstützt, automatisch JSON parst, Timeout-Konfiguration hat und konsistentes Error-Handling über alle Browser bietet. Eine Axios-Instanz mit einer baseURL, Default-Headern und Interceptors bildet das Fundament des API-Layers. Statt mehrere Axios-Instanzen zu erstellen, wird eine zentrale Instanz über die gesamte Anwendung geteilt.
Request-Interceptors in Vue REST API-Projekten übernehmen das automatische Anhängen von Authorization-Token, die Aktualisierung abgelaufener Tokens vor dem Request und das Setzen von Correlation-IDs für Server-Logging. Response-Interceptors normalisieren Fehlerstrukturen aus unterschiedlichen Backend-Services zu einem einheitlichen Frontend-Fehlerformat. Das bedeutet: Egal ob das Backend eine 422 Unprocessable Entity mit einer validierten Fehlerstruktur zurückgibt oder ein Load Balancer eine 503 Service Unavailable, der Response-Interceptor transformiert beide in eine Frontend-konforme Fehlerstruktur.
4. Repository-Pattern: API-Calls kapseln und testbar machen
Das Repository-Pattern ist in Vue REST API-Projekten die sauberste Art, API-Calls zu kapseln. Ein Repository ist eine Klasse oder ein Objekt mit Methoden für jede API-Operation: getProduct(id), getProducts(filter), createProduct(data), updateProduct(id, data), deleteProduct(id). Jede Methode führt den HTTP-Request durch, wendet den Mapper an und gibt das Domain-Modell zurück. Der Aufrufer – das Composable oder der Pinia-Store – arbeitet nur mit Domain-Modellen, nie mit rohen API-Antworten.
Der größte Vorteil des Repository-Patterns in Vue REST API-Tests ist die einfache Mockbarkeit. Das Repository-Interface beschreibt die Methoden, eine Testimplementierung gibt vordefinierte Werte zurück, ohne echte HTTP-Requests zu machen. Composables und Pinia-Stores, die das Repository als Dependency erhalten, können in Unit-Tests vollständig mit einem Mock-Repository getestet werden. Das ersetzt MSW für Unit-Tests und ist oft schlanker in der Konfiguration.
5. Error-Mapping: von HTTP-Fehlern zu App-Fehlern
In einer Vue REST API-Architektur gibt es verschiedene Fehlerklassen mit unterschiedlichen UI-Anforderungen. Ein 401 Unauthorized erfordert einen Redirect zur Login-Seite oder einen Token-Refresh. Ein 422 Unprocessable Entity mit Validierungsfehlern soll die betroffenen Formularfelder markieren. Ein 404 Not Found soll eine leere Zustandsanzeige auslösen. Ein 503 Service Unavailable soll einen Retry-Mechanismus aktivieren. Diese unterschiedlichen Fehlertypen erfordern ein strukturiertes Error-Mapping, das aus dem generischen HTTP-Fehler eine typisierte App-Ausnahme macht, die die Komponente weiterverarbeiten kann.
Das Error-Mapping in Vue REST API-Projekten implementiert man als Enum oder Klassen-Hierarchie: NetworkError für Konnektivitätsprobleme, AuthError für 401/403, ValidationError für 422 mit Feld-Fehlern, NotFoundError für 404 und ServerError für 5xx. Der Axios-Response-Interceptor überprüft den Status-Code und den Response-Body und wirft die passende typisierte Fehlerklasse. Composables und Stores fangen diese typisierten Fehler und reagieren mit der passenden UI-Reaktion, statt generische catch(err)-Blöcke zu haben, die nicht wissen, was sie anzeigen sollen.
// src/api/errors.ts
// Typed error hierarchy for structured error mapping in Vue REST API layers
export class AppError extends Error {
constructor(message: string, public readonly code: string) {
super(message)
this.name = 'AppError'
}
}
export class NetworkError extends AppError {
constructor() { super('Keine Verbindung zum Server', 'NETWORK_ERROR') }
}
export class AuthError extends AppError {
constructor() { super('Nicht autorisiert', 'AUTH_ERROR') }
}
export class ValidationError extends AppError {
constructor(public readonly fieldErrors: Record<string, string[]>) {
super('Validierungsfehler', 'VALIDATION_ERROR')
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} wurde nicht gefunden`, 'NOT_FOUND')
}
}
export class RetryableError extends AppError {
constructor(public readonly retryAfterMs: number = 1000) {
super('Server vorübergehend nicht erreichbar', 'RETRYABLE')
}
}
// src/api/http-client.ts
// Axios instance with response interceptor for error mapping
import axios from 'axios'
import { AuthError, NetworkError, NotFoundError, RetryableError, ValidationError } from './errors'
export const httpClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10_000,
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
})
httpClient.interceptors.response.use(
(response) => response,
(error) => {
// No response — network error or timeout
if (!error.response) throw new NetworkError()
const { status, data } = error.response
const retryAfter = error.response.headers['retry-after']
switch (true) {
case status === 401 || status === 403:
throw new AuthError()
case status === 404:
throw new NotFoundError(data?.resource ?? 'Ressource')
case status === 422:
throw new ValidationError(data?.errors ?? {})
case status === 429 || status >= 500:
throw new RetryableError(retryAfter ? parseInt(retryAfter) * 1000 : 1000)
default:
throw new AppError(data?.message ?? 'Unbekannter Fehler', `HTTP_${status}`)
}
}
)
6. Retry-Strategien: Exponential Backoff und idempotente Requests
Retry-Logik ist in Vue REST API-Implementierungen für produktionstaugliche Anwendungen unverzichtbar. Transiente Netzwerkfehler, kurze Serverausfälle und Rate-Limiting sind in realen Produktionsumgebungen alltäglich. Eine Retry-Strategie mit Exponential Backoff sendet nach einem fehlgeschlagenen Request nach einer Wartezeit erneut – und verdoppelt die Wartezeit bei jedem weiteren Fehlschlag. Nach einer definierten Anzahl von Versuchen wird der Fehler weitergegeben. Dieses Muster verhindert, dass kurzfristige Serverprobleme zu einem Fehler für den User werden.
Die wichtigste Einschränkung der Retry-Logik in Vue REST API-Projekten: Nur idempotente Requests dürfen automatisch wiederholt werden. GET-, HEAD- und DELETE-Requests können sicher wiederholt werden. POST-Requests, die eine Ressource erstellen, sollten nur mit Idempotenz-Tokens wiederholt werden – andernfalls würde bei einem Retry eine Ressource doppelt angelegt. PUT- und PATCH-Requests sind in der Regel idempotent und können sicher wiederholt werden. Die Retry-Implementierung muss diese Unterscheidung beachten.
7. useApi-Composable: Ladezustand und Fehler reaktiv verwalten
Das useApi-Composable ist das Verbindungsstück zwischen dem Repository-Layer und der Vue-Komponente in einer Vue REST API-Architektur. Es verwaltet reaktiv drei Zustände: data für das Ergebnis des API-Calls, loading für den Ladezustand und error für aufgetretene Fehler. Diese drei Refs sind das Minimum, das jede Komponente benötigt, um einen API-Call korrekt darzustellen. Das Composable übernimmt außerdem das Auslösen des Calls, die Fehlerbehandlung und optional das automatische Laden beim Mount der Komponente.
Ein generisches useApi-Composable, das jeden asynchronen Aufruf wrappen kann, reduziert Boilerplate erheblich. Statt in jedem Composable denselben loading.value = true; try { ... } catch { ... } finally { loading.value = false }-Pattern zu implementieren, delegiert man diesen Ablauf an das generische useApi. Das Ergebnis: Spezifische Composables wie useProduct(id) oder useProductList(filter) sind auf die fachliche Logik fokussiert und delegieren die technische Request-Verwaltung an das generische Composable. Das macht spezifische Composables deutlich schlanker und einheitlicher in der Fehlerbehandlung.
// src/composables/useApi.ts
// Generic composable for any async API call with loading, error, and data state
import { ref, type Ref } from 'vue'
import { AppError, NetworkError, RetryableError } from '@/api/errors'
interface UseApiReturn<T> {
data: Ref<T | null>
loading: Ref<boolean>
error: Ref<AppError | null>
execute: (...args: unknown[]) => Promise<void>
}
export function useApi<T>(
fn: (...args: unknown[]) => Promise<T>,
options: { immediate?: boolean; retries?: number } = {}
): UseApiReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>
const loading = ref(false)
const error = ref<AppError | null>(null)
async function executeWithRetry(retryCount: number, ...args: unknown[]): Promise<T> {
try {
return await fn(...args)
} catch (err) {
if (err instanceof RetryableError && retryCount > 0) {
// Exponential backoff: 1s, 2s, 4s...
const delay = err.retryAfterMs * Math.pow(2, (options.retries ?? 3) - retryCount)
await new Promise(resolve => setTimeout(resolve, delay))
return executeWithRetry(retryCount - 1, ...args)
}
throw err
}
}
async function execute(...args: unknown[]) {
loading.value = true
error.value = null
try {
data.value = await executeWithRetry(options.retries ?? 0, ...args)
} catch (err) {
error.value = err instanceof AppError
? err
: new NetworkError()
} finally {
loading.value = false
}
}
return { data, loading, error, execute }
}
// Usage in a feature composable:
// const { data: product, loading, error, execute: loadProduct } = useApi(
// (id: number) => productRepository.getProduct(id),
// { retries: 3 }
// )
8. Optimistische UI ohne GraphQL: manueller State-Rollback
Optimistische UI ist in Vue REST API-Projekten ohne Apollo möglich, erfordert aber manuellen Rollback-Code. Das Prinzip: Der lokale State wird sofort aktualisiert, als ob der API-Call bereits erfolgreich war. Gleichzeitig wird der API-Call asynchron ausgeführt. Wenn der Call erfolgreich ist, wird der optimistische State durch die echte Server-Antwort ersetzt. Wenn der Call fehlschlägt, wird der alte State wiederhergestellt und dem User eine Fehlermeldung angezeigt. Das Ergebnis ist eine reaktionsschnelle UI, die sich sofort anfühlt, auch wenn die Serververbindung langsam ist.
Die Implementierung in Vue REST API-Projekten folgt einem klaren Muster: Vor dem API-Call wird ein Snapshot des aktuellen States erstellt. Der optimistische State wird sofort in den Store geschrieben. Der API-Call wird ausgeführt. Bei Erfolg wird der Snapshot verworfen. Bei Fehler wird der Snapshot wiederhergestellt und der Store auf den alten State zurückgesetzt. Dieses Muster funktioniert gut für Aktionen wie Toggles, Statusänderungen und Delete-Operationen, bei denen das Ergebnis mit hoher Wahrscheinlichkeit dem Optimismus entspricht.
9. HTTP-Client-Optionen im Vergleich
Die Wahl des HTTP-Clients beeinflusst die Vue REST API-Architektur erheblich. Unterschiedliche Clients haben unterschiedliche Stärken und Schwächen, die je nach Projektanforderungen abgewogen werden müssen.
| Client | Interceptors | Bundle-Größe | Stärken |
|---|---|---|---|
| Axios | Ja (Request + Response) | ~13 KB gzip | Vollständig, ausgereift, große Community |
| native fetch | Nein (manuell) | 0 KB (Browser-built-in) | Kein Dependency, native API, SSR-kompatibel |
| ofetch | Ja (onRequest, onResponse) | ~3 KB gzip | Nuxt-native, modern API, klein |
| ky | Ja (Hooks) | ~5 KB gzip | Fetch-basiert, Retry built-in, modern |
| TanStack Query | Ja (global defaults) | ~12 KB gzip | Caching, Retry, Stale-While-Revalidate |
Für typische Vue REST API-Projekte mittlerer Größe empfiehlt sich Axios, wenn das Team mit ihm vertraut ist und Interceptors intensiv genutzt werden. ofetch ist die erste Wahl in Nuxt-3-Projekten. ky eignet sich für Projekte, die Fetch-API nutzen wollen, aber eingebautes Retry und Hooks brauchen. TanStack Query für Vue (@tanstack/vue-query) ist eine vollständige Lösung für Server-State-Management mit Caching, Retry, Background-Refetching und Stale-While-Revalidate – ein ernstzunehmender Ersatz für Apollo in REST-Projekten.
Mironsoft
Vue REST API · TypeScript · Frontend-Architektur · API-Integration
Vue REST API-Schicht strukturiert aufbauen?
Wir konzipieren und implementieren skalierbare Vue REST API-Layer mit DTOs, strukturiertem Error-Mapping, Retry-Logik und Repository-Pattern für langfristig wartbare Frontend-Architekturen.
API-Architektur
Repository-Pattern, DTO-Mapping und HTTP-Client-Setup für wartbare API-Schichten
Fehlerbehandlung
Strukturiertes Error-Mapping, typisierte Fehlerklassen und konsistente UI-Reaktionen
Robustheit
Retry mit Exponential Backoff, optimistische UI und Idempotenz-Tokens
10. Zusammenfassung
Eine professionelle Vue REST API-Architektur trennt klar zwischen HTTP-Client, Repository, DTO-Mapper und Composable. DTOs beschreiben die API-Antwort exakt, Domain-Modelle beschreiben die interne Frontend-Struktur, und Mapper überbrücken den Unterschied. Axios-Interceptors normalisieren Fehlerstrukturen zu typisierten Fehlerklassen, bevor sie den Repository-Layer verlassen. Das Repository kapselt API-Calls und macht sie ohne HTTP-Requests testbar. Das useApi-Composable verwaltet Ladezustand, Daten und Fehler reaktiv und einheitlich über alle Feature-Composables.
Retry-Logik mit Exponential Backoff verhindert, dass transiente Serverfehler als User-Fehler erscheinen. Optimistische UI mit manuellem Rollback macht Aktionen sofort reaktionsschnell, ohne auf die Server-Antwort zu warten. Die Investition in diesen strukturierten Vue REST API-Layer zahlt sich spätestens dann aus, wenn das Backend-Team Feldnamen ändert, einen neuen API-Versionspfad einführt oder die Fehlerstruktur überarbeitet – dann ist es eine einzige Anpassung im Mapper oder Interceptor statt ein Refactoring durch alle Komponenten.
Vue REST API — Das Wichtigste auf einen Blick
DTO-Mapping
API-DTOs beschreiben Server-Antworten, Domain-Modelle die interne Struktur. Mapper isolieren Backend-Änderungen auf eine einzige Stelle.
Error-Mapping
Axios-Interceptor transformiert HTTP-Fehler in typisierte Fehlerklassen – AuthError, ValidationError, RetryableError. Komponenten reagieren strukturiert.
Retry & Robustheit
Exponential Backoff für transiente Fehler. Nur idempotente Requests (GET, PUT, DELETE) automatisch wiederholen. POST nur mit Idempotenz-Token.
Repository & Composable
Repository kapselt API-Calls, gibt Domain-Modelle zurück. useApi-Composable verwaltet loading, data, error reaktiv und einheitlich.