JavaScript Fortgeschritten

Fetch API & REST verstehen

Fetch API & REST verstehen

Die Fetch API ist der moderne Standard für HTTP-Requests in JavaScript. In diesem Tutorial lernst du, wie du mit Servern und APIs kommunizierst.

Was ist die Fetch API?

fetch() ist die native Browser-Funktion für Netzwerk-Anfragen:

// Einfachster fetch
fetch("https://api.example.com/users")
    .then(response => response.json())
    .then(data => console.log(data));

Mit async/await (empfohlen)

async function getUsers() {
    const response = await fetch("https://api.example.com/users");
    const data = await response.json();
    return data;
}

REST API Grundlagen

REST (Representational State Transfer) ist ein Standard für API-Design:

HTTP-MethodeAktionBeispiel
GETDaten lesenAlle User holen
POSTDaten erstellenNeuen User anlegen
PUTDaten komplett ersetzenUser aktualisieren
PATCHDaten teilweise ändernNur Name ändern
DELETEDaten löschenUser löschen

Typische REST-Endpunkte

GET    /api/users          → Alle User
GET    /api/users/1        → User mit ID 1
POST   /api/users          → Neuen User erstellen
PUT    /api/users/1        → User 1 komplett ersetzen
PATCH  /api/users/1        → User 1 teilweise ändern
DELETE /api/users/1        → User 1 löschen

GET - Daten laden

Einfacher GET-Request

async function getUsers() {
    const response = await fetch("https://api.example.com/users");
    const users = await response.json();
    return users;
}

// Verwenden
const users = await getUsers();
console.log(users);

Mit Query-Parametern

// URL mit Parametern bauen
const params = new URLSearchParams({
    page: 1,
    limit: 10,
    sort: "name"
});

const response = await fetch(`https://api.example.com/users?${params}`);
// URL: https://api.example.com/users?page=1&limit=10&sort=name

Response prüfen

async function getUser(id) {
    const response = await fetch(`https://api.example.com/users/${id}`);

    // Status prüfen!
    if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return response.json();
}

POST - Daten senden

async function createUser(userData) {
    const response = await fetch("https://api.example.com/users", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify(userData)
    });

    if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }

    return response.json();
}

// Verwenden
const newUser = await createUser({
    name: "Max Mustermann",
    email: "max@example.com"
});

console.log("Erstellt:", newUser);

PUT/PATCH - Daten aktualisieren

PUT - Komplett ersetzen

async function updateUser(id, userData) {
    const response = await fetch(`https://api.example.com/users/${id}`, {
        method: "PUT",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify(userData)
    });

    if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }

    return response.json();
}

// Alle Felder müssen angegeben werden!
await updateUser(1, {
    name: "Max",
    email: "max@example.com",
    role: "admin"
});

PATCH - Teilweise ändern

async function patchUser(id, partialData) {
    const response = await fetch(`https://api.example.com/users/${id}`, {
        method: "PATCH",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify(partialData)
    });

    if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }

    return response.json();
}

// Nur das ändern was nötig ist
await patchUser(1, { name: "Maximilian" });

DELETE - Daten löschen

async function deleteUser(id) {
    const response = await fetch(`https://api.example.com/users/${id}`, {
        method: "DELETE"
    });

    if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }

    // DELETE gibt oft keinen Body zurück
    return response.ok;
}

// Verwenden
if (await deleteUser(1)) {
    console.log("User gelöscht");
}

Das Response-Objekt

const response = await fetch(url);

// Status
console.log(response.status);       // 200, 404, 500, ...
console.log(response.statusText);   // "OK", "Not Found", ...
console.log(response.ok);           // true wenn 200-299

// Headers
console.log(response.headers.get("Content-Type"));
console.log(response.headers.get("X-Total-Count"));

// Body lesen (nur einmal möglich!)
const json = await response.json();    // Als JSON
const text = await response.text();    // Als Text
const blob = await response.blob();    // Als Blob (Dateien)
const buffer = await response.arrayBuffer();  // Als ArrayBuffer

Headers setzen

const response = await fetch(url, {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Authorization": "Bearer eyJhbGci...",
        "X-Custom-Header": "Wert",
        "Accept": "application/json"
    },
    body: JSON.stringify(data)
});

Mit Headers-Objekt

const headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append("Authorization", `Bearer ${token}`);

const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data)
});

Fehlerbehandlung

HTTP-Fehler prüfen

async function fetchWithError(url) {
    const response = await fetch(url);

    if (!response.ok) {
        // Versuche Fehler-Details zu lesen
        let errorMessage;
        try {
            const errorData = await response.json();
            errorMessage = errorData.message || response.statusText;
        } catch {
            errorMessage = response.statusText;
        }

        throw new Error(`HTTP ${response.status}: ${errorMessage}`);
    }

    return response.json();
}

Netzwerk-Fehler

async function safeFetch(url) {
    try {
        const response = await fetch(url);

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }

        return await response.json();

    } catch (error) {
        if (error.name === "TypeError") {
            // Netzwerk-Fehler (offline, CORS, etc.)
            console.error("Netzwerk-Fehler:", error.message);
        } else {
            // Anderer Fehler
            console.error("Fehler:", error.message);
        }
        throw error;
    }
}

Timeout implementieren

fetch hat keinen eingebauten Timeout, aber wir können einen bauen:

async function fetchWithTimeout(url, options = {}, timeout = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal
        });
        clearTimeout(timeoutId);
        return response;
    } catch (error) {
        clearTimeout(timeoutId);
        if (error.name === "AbortError") {
            throw new Error("Request timeout");
        }
        throw error;
    }
}

// Verwendung
try {
    const response = await fetchWithTimeout("/api/data", {}, 3000);
    const data = await response.json();
} catch (error) {
    console.log(error.message);  // "Request timeout"
}

Request abbrechen

const controller = new AbortController();

// Request starten
const fetchPromise = fetch("/api/data", {
    signal: controller.signal
});

// Später abbrechen
controller.abort();

// Im UI
const controller = new AbortController();
const abortBtn = document.querySelector("#abort");

abortBtn.addEventListener("click", () => {
    controller.abort();
});

try {
    const response = await fetch(url, { signal: controller.signal });
} catch (error) {
    if (error.name === "AbortError") {
        console.log("Request abgebrochen");
    }
}

API-Client erstellen

Einfacher Client

const API_BASE = "https://api.example.com";

const api = {
    async get(endpoint) {
        const response = await fetch(`${API_BASE}${endpoint}`);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
    },

    async post(endpoint, data) {
        const response = await fetch(`${API_BASE}${endpoint}`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data)
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
    },

    async put(endpoint, data) {
        const response = await fetch(`${API_BASE}${endpoint}`, {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data)
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
    },

    async delete(endpoint) {
        const response = await fetch(`${API_BASE}${endpoint}`, {
            method: "DELETE"
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.ok;
    }
};

// Verwendung
const users = await api.get("/users");
const newUser = await api.post("/users", { name: "Max" });
await api.put("/users/1", { name: "Max", email: "max@test.de" });
await api.delete("/users/1");

Erweiterter Client mit Auth

function createApiClient(baseUrl, getToken) {
    async function request(endpoint, options = {}) {
        const url = `${baseUrl}${endpoint}`;

        const config = {
            ...options,
            headers: {
                "Content-Type": "application/json",
                ...options.headers
            }
        };

        // Token hinzufügen wenn vorhanden
        const token = getToken();
        if (token) {
            config.headers.Authorization = `Bearer ${token}`;
        }

        const response = await fetch(url, config);

        if (!response.ok) {
            const error = new Error(`HTTP ${response.status}`);
            error.status = response.status;
            try {
                error.data = await response.json();
            } catch {}
            throw error;
        }

        // Leere Response behandeln
        const text = await response.text();
        return text ? JSON.parse(text) : null;
    }

    return {
        get: (endpoint) => request(endpoint),
        post: (endpoint, data) => request(endpoint, {
            method: "POST",
            body: JSON.stringify(data)
        }),
        put: (endpoint, data) => request(endpoint, {
            method: "PUT",
            body: JSON.stringify(data)
        }),
        patch: (endpoint, data) => request(endpoint, {
            method: "PATCH",
            body: JSON.stringify(data)
        }),
        delete: (endpoint) => request(endpoint, { method: "DELETE" })
    };
}

// Verwendung
const api = createApiClient(
    "https://api.example.com",
    () => localStorage.getItem("token")
);

const users = await api.get("/users");

FormData senden

Für Datei-Uploads und Formulare:

const form = document.querySelector("form");

form.addEventListener("submit", async (e) => {
    e.preventDefault();

    const formData = new FormData(form);

    // Datei hinzufügen
    const fileInput = document.querySelector('input[type="file"]');
    formData.append("avatar", fileInput.files[0]);

    // KEIN Content-Type Header setzen! Browser macht das automatisch
    const response = await fetch("/api/upload", {
        method: "POST",
        body: formData
    });

    const result = await response.json();
    console.log("Upload erfolgreich:", result);
});

Parallele Requests

// Alle gleichzeitig
const [users, posts, comments] = await Promise.all([
    api.get("/users"),
    api.get("/posts"),
    api.get("/comments")
]);

// Mit Fehlertoleranz
const results = await Promise.allSettled([
    api.get("/users"),
    api.get("/maybe-fails"),
    api.get("/posts")
]);

const successfulData = results
    .filter(r => r.status === "fulfilled")
    .map(r => r.value);

Praktisches Beispiel: User-Management

const userService = {
    baseUrl: "/api/users",

    async getAll(filters = {}) {
        const params = new URLSearchParams(filters);
        const response = await fetch(`${this.baseUrl}?${params}`);
        if (!response.ok) throw new Error("Laden fehlgeschlagen");
        return response.json();
    },

    async getById(id) {
        const response = await fetch(`${this.baseUrl}/${id}`);
        if (response.status === 404) return null;
        if (!response.ok) throw new Error("Laden fehlgeschlagen");
        return response.json();
    },

    async create(userData) {
        const response = await fetch(this.baseUrl, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(userData)
        });
        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.message || "Erstellen fehlgeschlagen");
        }
        return response.json();
    },

    async update(id, userData) {
        const response = await fetch(`${this.baseUrl}/${id}`, {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(userData)
        });
        if (!response.ok) throw new Error("Update fehlgeschlagen");
        return response.json();
    },

    async delete(id) {
        const response = await fetch(`${this.baseUrl}/${id}`, {
            method: "DELETE"
        });
        if (!response.ok) throw new Error("Löschen fehlgeschlagen");
        return true;
    }
};

// Verwendung
const users = await userService.getAll({ role: "admin" });
const user = await userService.getById(1);
const newUser = await userService.create({ name: "Max", email: "max@test.de" });
await userService.update(1, { name: "Maximilian" });
await userService.delete(1);

Zusammenfassung

MethodeVerwendung
GETDaten lesen
POSTDaten erstellen
PUTKomplett ersetzen
PATCHTeilweise ändern
DELETELöschen
// Basis-Pattern
const response = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data)
});

if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();

Übung: Baue einen Blog-API-Client mit Methoden für Posts und Kommentare. Nutze die JSONPlaceholder API: https://jsonplaceholder.typicode.com