Erstellung einer NextJS-Website mit einem NestJS-Backend

Erstellung einer NextJS-Website mit einem NestJS-Backend

Datum
25.1.2026

Im letzten Artikel haben wir das Fundament gelegt: ein VPS, nginx als Reverse-Proxy, ein NestJS-Backend mit Firebase-basierter Authentifizierung und eine MySQL-Datenbank. Das Backend kann Anfragen annehmen, prüft Tokens und weiß, wie ToDos abgespeichert werden. Was jetzt noch fehlt, ist die Schicht, mit der echte Menschen interagieren — eine Website, über die du dich einloggen und deine ToDos sehen, anlegen, bearbeiten und löschen kannst.

Genau dafür ist dieser Artikel da. Wir bauen eine minimalistische NextJS-Website, die genau das macht: Login/Logout per Firebase, Abruf der ToDos vom Backend (mit dem benötigten Firebase-ID-Token), Erstellen neuer ToDos, Markieren als erledigt und Löschen. Minimalistisch heißt hier bewusst: kein User-Profil, keine fancy Animationen — stattdessen klarer, leicht nachvollziehbarer Code, den auch Einsteiger direkt nachbauen können.

Warum NextJS? Weil es zwei Stärken vereint, die für viele Projekte nützlich sind: moderne React-Entwicklung (komponentenbasiert) und die Möglichkeit zu serverseitiger Darstellung (falls wir das später wollen). In unserem Fall brauchen wir aber eine wichtige Fähigkeit von NextJS: die App läuft im Browser, und dort holen wir das Firebase-ID-Token des eingeloggten Users, das wir per Authorization: Bearer <token>an unser NestJS-Backend schicken. Weil die geschützten Endpunkte ein gültiges Token erwarten, ist die Firebase-Integration kein optionaler Schritt — sie muss direkt am Anfang passieren, damit die API-Calls funktionieren. Du kannst natürlich auch im Backend den FirebaseGuard von deinen Endpunkten entfernen. Dann kannst du den Schritt der Firebase-Integration überspringen und dich nur auf die ToDos konzentrieren.

Was genau lernst du in diesem Artikel?

Am Ende dieses Artikels kannst du:

  • eine NextJS-App aufsetzen und die Projektstruktur verstehen,

  • Firebase Client-SDK einbinden und die E-Mail/Passwort-Authentifizierung nutzen,

  • einen globalen AuthProvider und einen useAuth()-Hook bauen, um den eingeloggten User und sein Token zentral verfügbar zu machen,

  • Axios so konfigurieren, dass bei jedem API-Request das Firebase-Token im Header mitgeschickt wird,

  • die ToDo-Liste vom Backend laden, neue ToDos anlegen, ToDos als erledigt markieren und löschen — jeweils über die echten, geschützten Endpunkte deines Backends.

Voraussetzungen (kurze Checkliste — was du bereithaben solltest)

Bevor du loslegst, stelle sicher, dass du Folgendes hast:

  • Ein laufender VPS mit deinem NestJS-Backend, erreichbar unter einer Domain oder IP (z. B. https://deine-domain.de).

  • Ein Firebase-Projekt mit aktivierter E-Mail/Passwort-Authentifizierung und die Client-Config (apiKey, authDomain, projectId etc.).

  • Node.js und npm auf deinem lokalen Rechner installiert.

  • Ein Code-Editor (z. B. VS Code) und ein Terminal.

  • Optional: Postman oder andere Tools, um API-Calls zu testen.

Wenn einer dieser Punkte fehlt, wirst du in den jeweiligen Abschnitten Hinweise finden, wie du es anlegst — ich gehe aber davon aus, dass Backend & Firebase-Projekt aus Artikel 2 und 3 bereits existieren.


Projekt einrichten & Next.js starten

Bevor wir loslegen und unsere ToDo-Website mit Leben füllen können, müssen wir erst einmal ein Next.js-Projekt aufsetzen. Keine Sorge: Das geht relativ schnell, und wenn du die Schritte befolgst, wirst du am Ende schon deine erste funktionierende Seite sehen.

1. Voraussetzungen

Stelle sicher, dass du Node.js auf deinem Rechner installiert hast. Du kannst das prüfen, indem du in deinem Terminal oder in der Eingabeaufforderung diesen Befehl eingibst:

node -v

Wenn dort eine Versionsnummer erscheint (z. B. v20.x.x), ist alles gut. Falls nicht, lade dir Node.js von der offiziellen Website herunter:
👉 https://nodejs.org

Zusätzlich empfiehlt es sich, npm oder yarn als Paketmanager zu nutzen. Npm ist automatisch bei Node.js dabei, du musst also nichts extra installieren.

2. Neues Next.js-Projekt erstellen

Next.js bringt einen eigenen Starter-Befehl mit, der uns die Projektstruktur automatisch anlegt. Wechsel im Terminal in den Ordner, in dem du dein Projekt haben möchtest, und gib Folgendes ein:

npx create-next-app@latest todo-website

Hier passiert Folgendes:

  • npx führt ein Paket direkt aus, ohne dass du es vorher installieren musst.

  • create-next-app ist das Tool von Next.js, um ein neues Projekt aufzusetzen.

  • todo-website ist der Name des Projekts. Den kannst du frei wählen.

Im Anschluss werden dir ein paar Fragen gestellt, z. B. ob du TypeScript verwenden möchtest, ob ESLint aktiviert sein soll usw. Für unser Projekt empfehle ich:

  • TypeScript: ja (das macht unser Projekt robuster und ist auch in der NestJS-Welt Standard).

  • ESLint: ja (hilft dir, sauberen Code zu schreiben).

  • App Router: ja (das ist der neue Standard von Next.js).

  • Alles andere kannst du mit den Standardeinstellungen übernehmen.

3. Projekt starten

Wechsle in den neu erstellten Ordner:

cd todo-website

Und starte den Entwicklungsserver:

npm run dev

Wenn alles klappt, kannst du nun in deinem Browser http://localhost:3000 öffnen und siehst die Standard-Startseite von Next.js. 🎉

4. Projektstruktur verstehen

Bevor wir weitermachen, schauen wir uns kurz an, was da eigentlich alles erstellt wurde. Die wichtigsten Teile:

  • app/: Hier legen wir unsere Seiten und Layouts an. Zum Beispiel wird es eine Seite geben, auf der die ToDos angezeigt werden.

  • public/: Hier kannst du statische Dateien ablegen (z. B. Bilder).

  • package.json: Hier stehen alle Abhängigkeiten und Skripte, die für dein Projekt benötigt werden.

  • next.config.js: Hier können wir spezielle Konfigurationen für Next.js vornehmen, das brauchen wir aber erstmal nicht.

5. Erste eigene Seite erstellen

Damit du ein Gefühl bekommst, wie einfach es ist, Seiten in Next.js anzulegen, probieren wir gleich etwas aus.

Lege im app/-Ordner eine neue Datei an, z. B. app/todos/page.tsx. Der Name page.tsx ist wichtig, denn so weiß Next.js, dass hier eine Seite gemeint ist.

Schreib folgenden Code hinein:

export default function TodosPage() {
  return (
    <div>
      <h1>Meine ToDos</h1>
      <p>Hier werden bald alle Aufgaben erscheinen.</p>
    </div>
  );
}

Wenn du nun http://localhost:3000/todos im Browser öffnest, erscheint deine neue Seite. 🎉

Damit sind wir mit den Grundlagen fertig: Du hast ein Next.js-Projekt erstellt, gestartet und deine erste eigene Seite gebaut. Im nächsten Kapitel kümmern wir uns dann um die Firebase-Integration, damit unsere Website weiß, welcher User eingeloggt ist.


Firebase einbinden & AuthProvider erstellen

Damit unsere ToDo-Website weiß, welcher Nutzer eingeloggt ist, brauchen wir eine Benutzerauthentifizierung. Dafür verwenden wir Firebase Authentication, weil es extrem einfach einzurichten ist und viele Login-Methoden unterstützt (E-Mail/Passwort, Google-Login usw.).

Wir gehen Schritt für Schritt vor:

1. Web-App in Firebase registrieren

Im Firebase-Projekt-Dashboard klickst du auf "App hinzufügen" und wählst das Web-Symbol (</>).

Gib deiner App einen Namen, z. B. todo-website.

  1. Aktiviere Firebase Hosting nicht – das brauchen wir nicht, wir hosten die Website auf unserem eigenen Server.

  2. Klicke auf "Registrieren".

Danach bekommst du ein Code-Snippet angezeigt, das ungefähr so aussieht:

const firebaseConfig = {
  apiKey: "AIza....",
  authDomain: "todo-app.firebaseapp.com",
  projectId: "todo-app",
  storageBucket: "todo-app.appspot.com",
  messagingSenderId: "123456789",
  appId: "1:123456789:web:abcdefg"
};

👉 Diese Daten brauchen wir gleich in unserem Projekt.

2. Firebase im Next.js-Projekt installieren

Wechsle in dein Projekt und installiere die Firebase-Pakete:

npm install firebase

Damit haben wir alles, um Firebase in Next.js zu nutzen.

3. Firebase konfigurieren

Lege in deinem Projekt einen neuen Ordner lib/ an. Dort erstellen wir eine Datei firebase.ts:

// lib/firebase.ts
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: "DEIN_API_KEY",
  authDomain: "DEIN_AUTH_DOMAIN",
  projectId: "DEIN_PROJECT_ID",
  storageBucket: "DEIN_STORAGE_BUCKET",
  messagingSenderId: "DEIN_SENDER_ID",
  appId: "DEIN_APP_ID",
};

// Firebase App initialisieren
const app = initializeApp(firebaseConfig);

// Firebase Auth exportieren
export const auth = getAuth(app);

⚠️ Wichtig: Die Daten aus firebaseConfig ersetzt du durch deine eigenen aus der Firebase-Konsole.

4. AuthProvider erstellen

Damit wir überall in unserer App wissen, ob ein User eingeloggt ist oder nicht, erstellen wir einen Provider.

Lege im Projektordner context/AuthContext.tsx an:

"use client";

import { createContext, useContext, useEffect, useState } from "react";
import { onAuthStateChanged, User } from "firebase/auth";
import { auth } from "@/lib/firebase";

// 1. Context erstellen
const AuthContext = createContext<{ user: User | null }>({ user: null });

// 2. Provider-Komponente
export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    // Firebase-Listener: Wird aufgerufen, wenn sich der Login-Status ändert
    const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
      setUser(firebaseUser);
    });

    return () => unsubscribe();
  }, []);

  return (
    <AuthContext.Provider value={{ user }}>
      {children}
    </AuthContext.Provider>
  );
}

// 3. Custom Hook für einfachen Zugriff
export function useAuth() {
  return useContext(AuthContext);
}

5. AuthProvider in der App verwenden

Damit der Provider überall gilt, binden wir ihn in app/layout.tsx ein:

import "./globals.css";
import { AuthProvider } from "@/context/AuthContext";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="de">
      <body>
        <AuthProvider>
          {children}
        </AuthProvider>
      </body>
    </html>
  );
}

6. Login und Logout

Damit sich ein Nutzer überhaupt anmelden kann, brauchen wir Buttons für Login und Logout.
In diesem Beispiel nutzen wir die Google-Anmeldung, weil das am einfachsten ist. Später kannst du das natürlich erweitern.

Erstelle eine neue Komponente:

/components/AuthButtons.tsx

"use client";

import { auth } from "@/lib/firebase";
import { GoogleAuthProvider, signInWithPopup, signOut } from "firebase/auth";
import { useAuth } from "@/contexts/AuthContext";

export default function AuthButtons() {
  const { user } = useAuth();

  const handleLogin = async () => {
    const provider = new GoogleAuthProvider();
    await signInWithPopup(auth, provider);
  };

  const handleLogout = async () => {
    await signOut(auth);
  };

  if (user) {
    return (
      <div>
        <p>Eingeloggt als: {user.email}</p>
        <button onClick={handleLogout}>Logout</button>
      </div>
    );
  }

  return (
    <div>
      <p>Du bist nicht eingeloggt</p>
      <button onClick={handleLogin}>Mit Google anmelden</button>
    </div>
  );
}

Damit hast du jetzt eine kleine Komponente, die entweder einen Login-Button oder die User-Info mit Logout-Button anzeigt.

Sollte es nicht auf Anhieb funktionieren, folge am besten dieser Dokumentation von Firebase : https://firebase.google.com/codelabs/firebase-nextjs?hl=de#5. Vielleicht ist deine Domain noch nicht bei Firebase autorisiert?

7. Aktuellen User anzeigen

Um zu prüfen, ob alles funktioniert, ändern wir unsere app/page.tsx so ab:

"use client";

import { useAuth } from "@/context/AuthContext";

export default function HomePage() {
  return (
    <div>
      <h1>Willkommen zu meiner ToDo-App</h1>
      <AuthButtons/>
    </div>
  );
}

👉 Wenn du dich jetzt in Firebase anmeldest (z. B. über die Firebase Console oder später über ein Login-Formular), siehst du direkt den User in deiner App.

8. Testen

Wenn du jetzt npm run dev ausführst und deine App im Browser öffnest, solltest du dich über den Google-Login einloggen können. Danach wird dein User oben angezeigt. Wenn du dich ausloggst, verschwindet die Anzeige wieder.

✅ Damit haben wir die Grundlage geschaffen: Unsere App kennt jetzt den aktuellen User.
Im nächsten Schritt werden wir diese Information nutzen, um unsere API-Endpunkte aufzurufen und die ToDos zu laden.


ToDo-Übersicht – Alle Aufgaben auf einen Blick

Jetzt wird es spannend, denn wir bauen die erste richtige Seite unserer Website: eine Übersicht über alle ToDos des eingeloggten Benutzers. Genau dafür haben wir bisher die Grundlagen geschaffen – wir haben eine Authentifizierung eingebaut und ein Backend mit einer ToDo-API. Nun geht es darum, die Daten aus dieser API abzurufen und im Frontend anzuzeigen.

Damit unsere App übersichtlich bleibt, legen wir eine eigene Seite an. In Next.js geschieht das, indem man einfach eine neue Datei im Ordner pages anlegt. Erstellen wir also die Datei todos.tsx. Diese Seite soll später die Übersicht enthalten, auf der alle Aufgaben des eingeloggten Users angezeigt werden.

Der grundlegende Aufbau dieser Seite sieht so aus:

// pages/todos.tsx
import { useEffect, useState } from "react";
import { useAuth } from "../hooks/AuthProvider";

export default function TodosPage() {
  const { user } = useAuth();
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    const fetchTodos = async () => {
      if (!user) return;

      const token = await user.getIdToken();
      const response = await fetch("http://deine-domain.de/todos", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      const data = await response.json();
      setTodos(data);
    };

    fetchTodos();
  }, [user]);

  return (
    <div>
      <h1>Deine ToDos</h1>
      <ul>
        {todos.map((todo: any) => (
          <li key={todo.id}>
            {todo.title} {todo.completed ? "✅" : "❌"}
          </li>
        ))}
      </ul>
    </div>
  );
}

Schauen wir uns in Ruhe an, was hier passiert. Ganz am Anfang importieren wir useEffect und useState. Diese beiden React-Hooks sind so etwas wie kleine Werkzeuge, die uns dabei helfen, Daten zu speichern und bestimmte Dinge beim Laden der Seite automatisch auszuführen. Mit useState legen wir einen kleinen Speicher an, in dem unsere ToDos landen, sobald wir sie vom Server bekommen. Mit useEffect sagen wir: „Sobald sich etwas Wichtiges verändert – in diesem Fall, wenn ein Benutzer eingeloggt ist – dann führe diese Funktion aus.“

In dieser Funktion fetchTodos passiert der eigentliche Zauber. Zuerst prüfen wir: Ist ein Benutzer eingeloggt? Falls nicht, ergibt es auch keinen Sinn, ToDos abzufragen. Wenn ein User vorhanden ist, holen wir uns über getIdToken() das sogenannte JWT-Token. Dieses Token ist so etwas wie ein digitaler Ausweis, der bestätigt: „Ja, dieser Benutzer ist wirklich bei Firebase authentifiziert.“ Dieses Token hängen wir dann an unseren Request an das Backend – und zwar im Header unter Authorization: Bearer <token>.

Unser NestJS-Backend überprüft dieses Token automatisch mit Hilfe des Firebase Guards. Wenn das Token gültig ist, werden die ToDos für genau diesen Benutzer aus der Datenbank geladen und zurückgeschickt. Im Frontend nehmen wir diese Daten entgegen, wandeln sie in ein JSON-Format um und speichern sie mit setTodos in unserem State.

Der letzte Teil ist die Darstellung. Mit einem einfachen <ul>-Element (eine ungeordnete Liste) zeigen wir jedes ToDo an. Innerhalb der Schleife (todos.map) gehen wir alle Aufgaben durch und listen sie nacheinander auf. Dabei geben wir den Titel aus und zeigen ein Häkchen, wenn die Aufgabe erledigt ist, oder ein Kreuz, wenn sie noch offen ist.

Wenn du dich jetzt einloggst und die Seite http://localhost:3000/todos aufrufst, solltest du die Aufgaben sehen, die zu deinem Benutzer gehören. Hast du noch keine erstellt, wird die Liste leer bleiben – aber das ist völlig in Ordnung, denn im nächsten Schritt kümmern wir uns darum, wie du neue ToDos anlegen kannst.

Damit hast du nun den ersten sichtbaren Beweis, dass dein System funktioniert: Ein User loggt sich ein, holt sich mit seinem Token die Daten vom Backend und sieht seine persönlichen Aufgaben im Browser. 🎉


ToDo-Erstellung – Neue Aufgaben hinzufügen

Bisher können wir uns die ToDos eines Benutzers anschauen, die in der Datenbank gespeichert sind. Aber noch ist unsere Liste wahrscheinlich leer, weil wir selbst noch keine Aufgaben erstellt haben. In diesem Kapitel fügen wir deshalb die Möglichkeit hinzu, direkt auf unserer Website ein neues ToDo zu erstellen.

Die Idee ist simpel: Wir bauen ein kleines Formular, in das der User den Titel einer Aufgabe eintragen kann. Sobald er das Formular abschickt, schicken wir die Daten an unser Backend. Dort wird das neue ToDo in die Datenbank eingetragen und anschließend automatisch wieder in unserer Übersicht angezeigt.

1. Formular in der todos.tsx-Seite einbauen

Wir öffnen wieder unsere Datei pages/todos.tsx und erweitern sie um ein Formular. Direkt oberhalb der Liste der ToDos fügen wir ein einfaches Eingabefeld und einen Button ein.

import { useEffect, useState } from "react";
import { useAuth } from "../hooks/AuthProvider";

export default function TodosPage() {
  const { user} = useAuth();
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState("");

 useEffect(() => {
    const fetchTodos = async () => {
      if (!user) return;

      const token = await user.getIdToken();
      const response = await fetch("http://deine-domain.de/todos", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      const data = await response.json();
      setTodos(data);
    };

    fetchTodos();
  }, [user]);

  const handleAddTodo = async (e: React.FormEvent) => {
    e.preventDefault(); // verhindert, dass die Seite neu geladen wird

    if (!newTodo.trim()) return; // Leere Eingaben ignorieren

    const token = await user.getIdToken();

    const response = await fetch("http://deine-domain.de/todos", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({ title: newTodo }),
    });

    if (response.ok) {
      const createdTodo = await response.json();
      setTodos([...todos, createdTodo]); // Neues ToDo zur Liste hinzufügen
      setNewTodo(""); // Eingabefeld wieder leeren
    }
  };

  return (
    <div>
      <h1>Deine ToDos</h1>

      <form onSubmit={handleAddTodo}>
        <input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Neue Aufgabe eingeben"
        />
        <button type="submit">Hinzufügen</button>
      </form>

      <ul>
        {todos.map((todo: any) => (
          <li key={todo.id}>
            {todo.title} {todo.completed ? "✅" : "❌"}
          </li>
        ))}
      </ul>
    </div>
  );
}

2. Was passiert hier eigentlich?

Zuerst haben wir mit useState eine neue Variable newTodo angelegt. Darin speichern wir immer den aktuellen Wert aus dem Eingabefeld. Immer wenn der User etwas tippt, aktualisieren wir diesen Wert mit setNewTodo.

Wenn der User das Formular abschickt (entweder mit Enter oder durch Klick auf den Button), wird die Funktion handleAddTodo ausgeführt. Dort passiert folgendes:

  1. Wir verhindern mit e.preventDefault(), dass der Browser die Seite neu lädt, was er normalerweise beim Absenden eines Formulars tun würde.

  2. Wir prüfen, ob wirklich Text eingegeben wurde. Falls das Feld leer ist, brechen wir ab.

  3. Wir holen uns wieder das Auth-Token des Users, damit das Backend weiß, dass dieser Request gültig ist.

  4. Wir machen einen POST-Request an die Adresse /todos. Das bedeutet, wir schicken Daten an den Server, um ein neues ToDo anzulegen. In den Body packen wir ein JSON mit dem Titel des neuen ToDos.

  5. Wenn der Server erfolgreich antwortet, bekommen wir das erstellte ToDo zurück. Dieses hängen wir an unsere bestehende Liste an (setTodos([...todos, createdTodo])), sodass der User sofort sieht, dass die neue Aufgabe hinzugefügt wurde.

  6. Am Ende setzen wir das Eingabefeld wieder zurück, damit der User gleich die nächste Aufgabe eintragen kann.

3. Testen

Jetzt kannst du dich wieder in deine App einloggen und zur Seite /todos gehen. Dort solltest du nun über das Eingabefeld neue Aufgaben erstellen können. Jede neue Aufgabe wird direkt ans Backend geschickt, in der Datenbank gespeichert und taucht sofort in deiner Liste auf.

Wenn du jetzt die Seite neu lädst, sollten deine Aufgaben immer noch da sein – das ist der Beweis, dass die Verbindung zwischen Frontend, Backend und Datenbank funktioniert.


Damit haben wir einen richtig wichtigen Schritt geschafft: Deine Website kann nicht nur Daten anzeigen, sondern auch neue Einträge erzeugen und speichern. Genau das macht eine App lebendig. Im nächsten Kapitel kümmern wir uns darum, bestehende ToDos zu bearbeiten, also eine Aufgabe als erledigt zu markieren.

ToDo bearbeiten – Aufgaben als erledigt markieren

Unsere ToDo-Liste zeigt jetzt bereits alle Aufgaben des eingeloggten Nutzers an. Doch bisher sind sie nur passiv sichtbar. In diesem Kapitel fügen wir endlich die Möglichkeit hinzu, Aufgaben aktiv zu verändern – genauer gesagt: sie als erledigt oder nicht erledigt zu markieren.
Das ist ein kleiner, aber wichtiger Schritt, denn er bringt echte Interaktivität in unsere Anwendung. Der Nutzer kann so direkt mit seiner Liste arbeiten, ohne zusätzliche Seiten oder Dialoge öffnen zu müssen.

1. Was bedeutet „bearbeiten“ in unserem Fall?

In vielen ToDo-Apps kann man Aufgaben umbenennen, priorisieren oder sogar farblich markieren.
Wir halten es bewusst minimalistisch: Unsere einzige Bearbeitungsfunktion besteht darin, dass der Nutzer ein ToDo abhaken kann, wenn es erledigt ist.
Technisch gesehen ändern wir damit das Feld completed in unserem ToDo-Objekt von false auf true – oder umgekehrt, wenn der Nutzer die Markierung wieder entfernt.

Diese Änderung schicken wir dann über einen sogenannten PATCH-Request an unser Backend, das die neue Information in der Datenbank speichert.

2. Der Aufbau der ToDo-Komponente

Schauen wir uns zuerst an, wie eine einzelne Aufgabe in unserem Frontend aussieht.
Jedes ToDo wird in der Übersicht mit einer Checkbox dargestellt, die genau diesen completed-Status widerspiegelt. Ist das ToDo erledigt, ist die Checkbox angehakt – andernfalls bleibt sie leer.

Hier ein einfaches Beispiel in React (TypeScript):

<li key={todo.id} className="flex items-center gap-2 py-1">
  <input
    type="checkbox"
    checked={todo.completed}
    onChange={() => toggleTodo(todo.id, !todo.completed)}
  />
  <span className={todo.completed ? "line-through text-gray-500" : ""}>
    {todo.title}
  </span>
</li>

👉 Wenn der Nutzer also auf die Checkbox klickt, wird toggleTodo() aufgerufen.
Wir übergeben dabei die ID der Aufgabe und den neuen Status (also das Gegenteil des aktuellen Werts).

3. Die toggleTodo()-Funktion

Jetzt kommt der eigentliche Kern: die Funktion, die den neuen Status an das Backend überträgt.
Wir wollen sicherstellen, dass die Änderung dauerhaft gespeichert wird und nicht nur im Browser sichtbar ist.

So könnte die Funktion aussehen:

const toggleTodo = async (id: string, completed: boolean) => {
  try {
    const token = await user.getIdToken();
    await fetch(`${process.env.NEXT_PUBLIC_API_URL}/todos/${id}`, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({ completed }),
    });
    // Nach dem Update: Liste neu laden
    fetchTodos();
  } catch (error) {
    console.error("Fehler beim Aktualisieren des ToDos:", error);
  }
};

Lass uns das kurz durchgehen:

Firebase-Token holen:
Damit das Backend weiß, wer die Anfrage stellt, holen wir das Token des eingeloggten Nutzers über user.getIdToken().

PATCH-Request senden:
Mit der fetch()-Funktion schicken wir einen HTTP-Request an unser NestJS-Backend.
Der Endpunkt /todos/:id erwartet dabei die ID der Aufgabe, die geändert werden soll.
In der Anfrage (Request Body) senden wir das JSON { completed: true } oder { completed: false }.

Liste neu laden:
Nach der Änderung rufen wir fetchTodos() auf, um die aktuelle ToDo-Liste neu zu laden. So ist sichergestellt, dass das UI den neuesten Stand zeigt.

Fehlerbehandlung:
Falls etwas schiefläuft (z. B. keine Internetverbindung oder abgelaufenes Token), wird eine Fehlermeldung in der Konsole ausgegeben.

4. Visuelles Feedback für den Nutzer

Damit der Nutzer sofort sieht, dass seine Aktion erfolgreich war, können wir im Frontend ein kleines visuelles Feedback einbauen.
Zum Beispiel können wir den Titel durchstreichen, wenn die Aufgabe erledigt ist, oder eine subtile Farbänderung hinzufügen.

<span
  className={`transition-colors duration-200 ${
    todo.completed
      ? "line-through text-gray-500"
      : "text-black"
  }`}
>
  {todo.title}
</span>

Die Kombination aus durchgestrichenem Text und grauer Farbe macht auf einen Blick klar, welche Aufgaben schon abgehakt sind.

5. Optional: Optimistische Updates

In unserer jetzigen Version aktualisieren wir das UI erst, nachdem die Antwort vom Server kommt.
Das ist sicher, kann aber kurzzeitig „träge“ wirken.
Ein moderner Ansatz ist das optimistische Update: Wir ändern den Zustand sofort im UI, bevor die Antwort da ist – und korrigieren nur, falls es einen Fehler gibt.

Das würde so aussehen:

const toggleTodo = async (id: string, completed: boolean) => {
  const updatedTodos = todos.map(todo =>
    todo.id === id ? { ...todo, completed } : todo
  );
  setTodos(updatedTodos);

  try {
    const token = await user.getIdToken();
    await fetch(`${process.env.NEXT_PUBLIC_API_URL}/todos/${id}`, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({ completed }),
    });
  } catch (error) {
    console.error("Fehler beim Aktualisieren:", error);
    fetchTodos(); // Falls Fehler → Daten neu laden
  }
};

So fühlt sich das Ganze flüssiger an, weil der Nutzer die Änderung sofort sieht.

6. Das Ergebnis

Wenn du jetzt deine Anwendung startest, dich einloggst und auf die Checkbox neben einem ToDo klickst, sollte es augenblicklich als erledigt markiert werden.
Im Hintergrund sendet deine Website eine Anfrage an das Backend, das die Änderung in der Datenbank speichert. Wenn du die Seite neu lädst, bleibt der Status erhalten.

Damit hast du nun eine interaktive und funktionale ToDo-Liste geschaffen, bei der der Nutzer seine Aufgaben direkt abhaken kann – ein wichtiger Meilenstein auf dem Weg zu einer vollständigen, vernetzten Anwendung.

ToDo löschen – Aufgaben entfernen

Unsere ToDo-Liste kann inzwischen erstellt, angezeigt und sogar bearbeitet werden.
Doch im echten Leben erledigen sich Aufgaben nicht nur, manchmal wollen wir sie auch einfach löschen.
Vielleicht war die Aufgabe nur ein Test, oder sie hat sich als nicht mehr relevant herausgestellt.
In diesem Kapitel lernst du, wie du deinen Nutzern die Möglichkeit gibst, ToDos dauerhaft aus der Datenbank zu entfernen – sicher, nachvollziehbar und einfach.

1. Was beim Löschen eigentlich passiert

Bevor wir mit dem Code starten, schauen wir uns kurz an, was „Löschen“ technisch bedeutet.

Wenn der Nutzer auf den Löschen-Button klickt, wird eine DELETE-Anfrage an unser NestJS-Backend geschickt.
Dort prüft der Server, ob der Nutzer überhaupt berechtigt ist, diese Aufgabe zu löschen (also ob sie wirklich ihm gehört).
Wenn das der Fall ist, wird der Eintrag aus der Datenbank entfernt.
Anschließend soll die Website automatisch die ToDo-Liste neu laden, damit die gelöschte Aufgabe verschwindet.

Das klingt kompliziert, ist aber in der Praxis nur eine kleine Ergänzung zu dem, was du bisher gebaut hast.

2. Den Lösch-Button im UI hinzufügen

Wir starten wieder im Frontend, also in unserer ToDo-Liste.
Neben jeder Aufgabe fügen wir nun einen kleinen Button mit einem 🗑️-Symbol oder einfach dem Text „Löschen“ hinzu.

So könnte das aussehen:

<li key={todo.id} className="flex items-center justify-between py-1">
  <div className="flex items-center gap-2">
    <input
      type="checkbox"
      checked={todo.completed}
      onChange={() => toggleTodo(todo.id, !todo.completed)}
    />
    <span className={todo.completed ? "line-through text-gray-500" : ""}>
      {todo.title}
    </span>
  </div>
  <button
    onClick={() => deleteTodo(todo.id)}
    className="text-red-500 hover:text-red-700 transition-colors"
  >
    Löschen
  </button>
</li>

👉 Damit hast du jetzt zu jedem ToDo einen Button, über den es gelöscht werden kann.
Fehlt nur noch die deleteTodo()-Funktion, die wir als Nächstes erstellen.

3. Die deleteTodo()-Funktion

Ähnlich wie bei der Bearbeitung (PATCH) schicken wir jetzt einen Request an unser Backend – diesmal mit der DELETE-Methode.
Auch hier müssen wir den Nutzer über das Firebase-Token authentifizieren, damit kein anderer Nutzer fremde Aufgaben löschen kann.

So sieht die Funktion aus:

const deleteTodo = async (id: string) => {
  const confirmed = window.confirm("Möchtest du dieses ToDo wirklich löschen?");
  if (!confirmed) return;

  try {
    const token = await user.getIdToken();
    await fetch(`${process.env.NEXT_PUBLIC_API_URL}/todos/${id}`, {
      method: "DELETE",
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    // Nach dem Löschen Liste neu laden
    fetchTodos();
  } catch (error) {
    console.error("Fehler beim Löschen des ToDos:", error);
  }
};

Gehen wir das Schritt für Schritt durch:

Sicherheitsabfrage:
Bevor wir das ToDo wirklich löschen, fragen wir mit window.confirm() nach.
So vermeiden wir, dass der Nutzer versehentlich auf „Löschen“ klickt und wichtige Aufgaben verschwinden.

Token holen:
Wieder holen wir uns über user.getIdToken() das Firebase-Token, damit das Backend weiß, welcher Nutzer die Anfrage stellt.

DELETE-Request:
Wir schicken einen HTTP-Request an den Endpunkt /todos/:id, wobei :id die eindeutige ID des ToDos ist. Den Endpunkt haben wir bereits im letzten Artikel der Reihe angelegt.

Liste aktualisieren:
Nach erfolgreichem Löschen wird mit fetchTodos() die aktuelle Liste neu geladen, sodass das gelöschte ToDo nicht mehr angezeigt wird.

Fehlerbehandlung:
Falls es ein Problem gibt (z. B. keine Verbindung oder ungültiges Token), wird eine Fehlermeldung in der Konsole ausgegeben.

4. Visuelles Feedback für den Nutzer

Optional kannst du deinem Nutzer auch ein kleines Feedback geben, wenn das Löschen erfolgreich war – zum Beispiel mit einem kurzen Hinweis oder einer grünen Einblendung.

alert("ToDo erfolgreich gelöscht!");

Oder, etwas moderner, du kannst mit einer State-Variable eine kleine temporäre Benachrichtigung einblenden, z. B. „Aufgabe entfernt ✅“.

5. Ergebnis

Wenn du jetzt deine Anwendung startest, solltest du Folgendes ausprobieren:

  1. Einloggen

  2. Ein oder mehrere ToDos anlegen

  3. Eines davon löschen

Nach dem Klick auf „Löschen“ erscheint die Sicherheitsabfrage.
Bestätigst du sie, wird das ToDo aus der Datenbank entfernt und verschwindet augenblicklich aus der Liste.

Damit hast du nun eine voll funktionsfähige CRUD-App (Create, Read, Update, Delete) – und das mit sauberer Firebase-Authentifizierung, einer eigenen Datenbank und einem modernen Frontend in NextJS.

Fazit

Damit hast du deine ToDo-Website erfolgreich zum Leben erweckt! 🎉
Deine Nutzer können sich nun sicher über Firebase Authentication anmelden, ihre eigenen Aufgaben erstellen, anzeigen, als erledigt markieren und löschen – alles mit einer modernen NextJS-Oberfläche, die direkt mit deinem NestJS-Backend kommuniziert.

Was du hier gebaut hast, ist mehr als nur eine kleine Demo: Es ist ein vollwertiger, skalierbarer Workflow, den du für jede Art von Projekt wiederverwenden kannst – ob für Notizen, Projekte oder Team-Boards.

Im nächsten Schritt geht es darum, das Ganze auch mobil erlebbar zu machen.
Wir widmen uns also der Flutter-App, die dieselbe API nutzt und die ToDos direkt vom Smartphone aus verwaltbar macht. 🚀

Kommentare

Bitte melde dich an, um einen Kommentar zu schreiben.

NextJS ToDo App mit Firebase Auth & NestJS Backend