Skip to content

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:

js
const response = await fetch("http://localhost:5000/aufgaben")
const daten = await response.json()
console.log(daten)
  • fetch(...) – schickt die Anfrage ab
  • await – wartet, bis die Antwort angekommen ist
  • .json() – liest die Antwort als JSON aus

Das await-Schlüsselwort funktioniert nur in async-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:

python
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 Frontend

Die Grundstruktur der HTML-Datei

Erstelle eine neue Datei index.html im gleichen Ordner wie app.py. Beginne mit dieser Grundstruktur:

html
<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üllen
  • neueAufgabe: "" – 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:

js
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:

html
<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:

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, rufe aufgabeHinzufuegen auf. Das .prevent verhindert, dass die Seite neu lädt.
  • v-model="neueAufgabe" – verknüpft das Eingabefeld mit der Variable neueAufgabe

Ergänze die Methode in JavaScript:

js
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 schicken
  • body: 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:

html
<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, wenn erledigt true ist
  • @change="toggleErledigt(aufgabe)" – wenn der Nutzer die Checkbox anklickt, rufe die Methode auf

Ergänze die Methode:

js
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:

html
<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:

js
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-Request
  • filter(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:

html
<style>
  .erledigt {
    text-decoration: line-through;
    color: gray;
  }
  li {
    list-style: none;
    margin: 5px 0;
  }
</style>

Und passe den Titel im HTML an:

html
<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:

html
<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:

AktionHTTP-Methodefetch()-Aufruf
Aufgaben ladenGETfetch(url)
Aufgabe erstellenPOSTfetch(url, { method: "POST", body: ... })
Aufgabe aktualisierenPUTfetch(url, { method: "PUT", body: ... })
Aufgabe löschenDELETEfetch(url, { method: "DELETE" })

Das Muster ist immer gleich: fetch() schickt die Anfrage, await wartet auf die Antwort, .json() liest die Daten aus.

Informatik & ICT Unterricht Neufeld