Frontend mit Vue.js
Im letzten Teil hast du eine API gebaut, die du mit Postman testen konntest. Jetzt schauen wir uns an, wie wir vom Client aus mit der API interagieren können.
Wie spricht eine Webseite mit einer API?
Bisher hast du Anfragen an die API mit Postman geschickt. Eine Webseite macht genau dasselbe – nur automatisch, wenn der Nutzer auf einen Button klickt oder die Seite lädt.
Dafür gibt es in JavaScript die eingebaute Funktion fetch(). Sie schickt eine HTTP-Anfrage an eine URL und gibt die Antwort zurück.
Hier ein einfaches Beispiel:
const response = await fetch("http://localhost:5000/aufgaben")
const daten = await response.json()
console.log(daten)fetch(...)– schickt die Anfrage abawait– wartet, bis die Antwort angekommen ist.json()– liest die Antwort als JSON aus
Das
await-Schlüsselwort funktioniert nur inasync-Funktionen. Das wirst du gleich in der Praxis sehen.
Was ist CORS?
Wenn eine Webseite eine Anfrage an einen anderen Server schickt (z.B. von index.html an localhost:5000), prüft der Browser zuerst: "Erlaubt der Server das?"
Diese Prüfung heisst CORS (Cross-Origin Resource Sharing). Falls der Server keine Erlaubnis gibt, blockiert der Browser die Anfrage – auch wenn der Server technisch antwortet.
Deshalb haben wir in app.py flask-cors eingebunden:
from flask_cors import CORS
CORS(app)Diese zwei Zeilen teilen dem Browser mit: "Ja, dieser Server erlaubt Anfragen von anderen Seiten."
Projektstruktur
Wir arbeiten mit zwei Dateien im gleichen Ordner:
meine-app/
├── app.py ← der Flask-Server (bereits vorhanden)
└── index.html ← unser neues FrontendDie Grundstruktur der HTML-Datei
Erstelle eine neue Datei index.html im gleichen Ordner wie app.py. Beginne mit dieser Grundstruktur:
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">
<!-- Hier kommt unsere App -->
</div>
<script type="module">
import { createApp } from 'vue'
createApp({
data() {
return {
aufgaben: [],
neueAufgabe: ""
}
}
}).mount("#app")
</script>
<style>
/* Styles kommen hier hin */
</style>Was haben wir hier?
aufgaben: []– eine leere Liste, die wir gleich mit Daten vom Server befüllenneueAufgabe: ""– der Text, den der Nutzer in das Eingabefeld tippt
Schritt 1: Aufgaben laden (GET)
Wenn die Seite lädt, soll sie alle Aufgaben vom Server holen. Dafür verwenden wir den mounted()-Hook – der wird automatisch aufgerufen, sobald Vue die App gestartet hat.
Ergänze dein Vue-Objekt:
createApp({
data() {
return {
aufgaben: [],
neueAufgabe: ""
}
},
mounted() {
this.ladeAufgaben()
},
methods: {
async ladeAufgaben() {
const response = await fetch("http://localhost:5000/aufgaben")
this.aufgaben = await response.json()
}
}
}).mount("#app")Und im HTML zeigen wir die Aufgaben mit v-for an:
<div id="app">
<h1>Meine Aufgaben</h1>
<ul>
<li v-for="aufgabe in aufgaben" :key="aufgabe.id">
{{ aufgabe.titel }}
</li>
</ul>
</div>Öffne jetzt index.html im Browser und du siehst die Aufgaben aus deiner API.
Stelle sicher, dass der Flask-Server läuft (
python app.py), bevor du die Seite öffnest.
Schritt 2: Neue Aufgabe erstellen (POST)
Jetzt fügen wir ein Formular hinzu. Wenn der Nutzer auf "Hinzufügen" klickt, schicken wir einen POST-Request an die API.
Ergänze das HTML:
<form @submit.prevent="aufgabeHinzufuegen">
<input v-model="neueAufgabe" placeholder="Neue Aufgabe eingeben..." />
<button type="submit">Hinzufügen</button>
</form>Was passiert hier?
@submit.prevent– wenn das Formular abgeschickt wird, rufeaufgabeHinzufuegenauf. Das.preventverhindert, dass die Seite neu lädt.v-model="neueAufgabe"– verknüpft das Eingabefeld mit der VariableneueAufgabe
Ergänze die Methode in JavaScript:
async aufgabeHinzufuegen() {
if (this.neueAufgabe.trim() === "") return
const response = await fetch("http://localhost:5000/aufgaben", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ titel: this.neueAufgabe })
})
const neueAufgabe = await response.json()
this.aufgaben.push(neueAufgabe)
this.neueAufgabe = ""
}Was passiert hier?
method: "POST"– wir schicken einen POST-Request (statt dem Standard GET)headers: { "Content-Type": "application/json" }– wir sagen dem Server, dass wir JSON schickenbody: JSON.stringify(...)– wir wandeln das JavaScript-Objekt in einen JSON-Text um- Nach der Antwort fügen wir die neue Aufgabe direkt der Liste hinzu – ohne die Seite neu zu laden
Schritt 3: Aufgabe als erledigt markieren (PUT)
Füge eine Checkbox neben jeder Aufgabe hinzu:
<li v-for="aufgabe in aufgaben" :key="aufgabe.id">
<input
type="checkbox"
:checked="aufgabe.erledigt"
@change="toggleErledigt(aufgabe)"
/>
{{ aufgabe.titel }}
</li>Was passiert hier?
:checked="aufgabe.erledigt"– die Checkbox ist angehakt, wennerledigttrue ist@change="toggleErledigt(aufgabe)"– wenn der Nutzer die Checkbox anklickt, rufe die Methode auf
Ergänze die Methode:
async toggleErledigt(aufgabe) {
await fetch(`http://localhost:5000/aufgaben/${aufgabe.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ erledigt: !aufgabe.erledigt })
})
aufgabe.erledigt = !aufgabe.erledigt
}Was passiert hier?
`http://localhost:5000/aufgaben/${aufgabe.id}`– wir fügen die ID der Aufgabe in die URL ein!aufgabe.erledigt– wir drehen den Wert um (true → false, false → true)- Nach dem Request aktualisieren wir den Wert direkt im Frontend, damit die Checkbox sofort reagiert
Schritt 4: Aufgabe löschen (DELETE)
Füge einen Löschen-Button neben jeder Aufgabe hinzu:
<li v-for="aufgabe in aufgaben" :key="aufgabe.id">
<input
type="checkbox"
:checked="aufgabe.erledigt"
@change="toggleErledigt(aufgabe)"
/>
{{ aufgabe.titel }}
<button @click="loescheAufgabe(aufgabe.id)">Löschen</button>
</li>Ergänze die Methode:
async loescheAufgabe(id) {
await fetch(`http://localhost:5000/aufgaben/${id}`, {
method: "DELETE"
})
this.aufgaben = this.aufgaben.filter(a => a.id !== id)
}Was passiert hier?
method: "DELETE"– wir schicken einen DELETE-Requestfilter(a => a.id !== id)– wir erstellen eine neue Liste ohne die gelöschte Aufgabe
Kleines CSS
Damit erledigte Aufgaben durchgestrichen aussehen, ergänze den Style-Bereich:
<style>
.erledigt {
text-decoration: line-through;
color: gray;
}
li {
list-style: none;
margin: 5px 0;
}
</style>Und passe den Titel im HTML an:
<span :class="{ erledigt: aufgabe.erledigt }">{{ aufgabe.titel }}</span>:class="{ erledigt: aufgabe.erledigt }" – Vue fügt die CSS-Klasse erledigt hinzu, wenn aufgabe.erledigt true ist.
Die komplette index.html
So sieht die fertige Datei aus:
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">
<h1>Meine Aufgaben</h1>
<form @submit.prevent="aufgabeHinzufuegen">
<input v-model="neueAufgabe" placeholder="Neue Aufgabe eingeben..." />
<button type="submit">Hinzufügen</button>
</form>
<ul>
<li v-for="aufgabe in aufgaben" :key="aufgabe.id">
<input
type="checkbox"
:checked="aufgabe.erledigt"
@change="toggleErledigt(aufgabe)"
/>
<span :class="{ erledigt: aufgabe.erledigt }">{{ aufgabe.titel }}</span>
<button @click="loescheAufgabe(aufgabe.id)">Löschen</button>
</li>
</ul>
</div>
<script type="module">
import { createApp } from 'vue'
createApp({
data() {
return {
aufgaben: [],
neueAufgabe: ""
}
},
mounted() {
this.ladeAufgaben()
},
methods: {
async ladeAufgaben() {
const response = await fetch("http://localhost:5000/aufgaben")
this.aufgaben = await response.json()
},
async aufgabeHinzufuegen() {
if (this.neueAufgabe.trim() === "") return
const response = await fetch("http://localhost:5000/aufgaben", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ titel: this.neueAufgabe })
})
const neueAufgabe = await response.json()
this.aufgaben.push(neueAufgabe)
this.neueAufgabe = ""
},
async toggleErledigt(aufgabe) {
await fetch(`http://localhost:5000/aufgaben/${aufgabe.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ erledigt: !aufgabe.erledigt })
})
aufgabe.erledigt = !aufgabe.erledigt
},
async loescheAufgabe(id) {
await fetch(`http://localhost:5000/aufgaben/${id}`, {
method: "DELETE"
})
this.aufgaben = this.aufgaben.filter(a => a.id !== id)
}
}
}).mount("#app")
</script>
<style>
.erledigt {
text-decoration: line-through;
color: gray;
}
li {
list-style: none;
margin: 5px 0;
}
</style>Zusammenfassung
Du hast gelernt, wie ein Frontend mit einer API kommuniziert:
| Aktion | HTTP-Methode | fetch()-Aufruf |
|---|---|---|
| Aufgaben laden | GET | fetch(url) |
| Aufgabe erstellen | POST | fetch(url, { method: "POST", body: ... }) |
| Aufgabe aktualisieren | PUT | fetch(url, { method: "PUT", body: ... }) |
| Aufgabe löschen | DELETE | fetch(url, { method: "DELETE" }) |
Das Muster ist immer gleich: fetch() schickt die Anfrage, await wartet auf die Antwort, .json() liest die Daten aus.