Zum Inhalt springen
Kontakt

Sebastian Nawrot
Dorneystr. 45
44149 Dortmund

Webentwicklung & Technik

React Todo-App mit MongoDB: MERN Stack Tutorial

Erweitere deine React Todo-App mit MongoDB und Mongoose. Lerne den MERN Stack mit persistenter Datenspeicherung.

Sebastian Nawrot
7 Min. Lesezeit
#React#MongoDB#MERN Stack#Mongoose#Node.js#Tutorial
React Todo-App mit MongoDB: MERN Stack Tutorial

Im ersten Teil dieser Serie haben wir eine Todo-App mit React und Express gebaut. Der In-Memory Datastore war praktisch zum Entwickeln, aber bei jedem Server-Neustart waren alle Daten weg.

In diesem Tutorial ersetzen wir das Array durch MongoDB - eine NoSQL-Datenbank, die perfekt zu JavaScript passt. Das Ergebnis ist der klassische MERN Stack: MongoDB, Express, React, Node.js.

Warum MongoDB?

MongoDB speichert Daten als JSON-ähnliche Dokumente. Das passt hervorragend zu JavaScript-Anwendungen:

  • Keine Schema-Definition nötig - flexibel für schnelle Entwicklung
  • JSON-Format - Daten sehen aus wie JavaScript-Objekte
  • Skalierbar - horizontal skalierbar für große Anwendungen
  • Kostenlose Cloud-Option - MongoDB Atlas bietet einen Free Tier

MongoDB Setup

Du hast zwei Optionen: MongoDB lokal installieren oder MongoDB Atlas (Cloud) nutzen. Für dieses Tutorial empfehle ich Atlas - es ist in 5 Minuten eingerichtet.

Option 1: MongoDB Atlas (empfohlen)

  1. Erstelle einen Account auf mongodb.com/atlas
  2. Erstelle einen kostenlosen Cluster (M0 Free Tier)
  3. Unter "Database Access" einen Benutzer anlegen
  4. Unter "Network Access" deine IP freigeben (oder 0.0.0.0/0 für überall)
  5. Klicke auf "Connect" und kopiere den Connection String

Der Connection String sieht so aus:

mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/todoapp?retryWrites=true&w=majority

Option 2: Lokale Installation

# macOS mit Homebrew
brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community

# Der lokale Connection String
mongodb://localhost:27017/todoapp

Mongoose installieren

Mongoose ist ein ODM (Object Document Mapper) für MongoDB. Es macht die Arbeit mit der Datenbank deutlich angenehmer durch Schemas, Validierung und mehr.

cd server
npm install mongoose dotenv

Wir installieren auch dotenv, um den Connection String sicher in einer Umgebungsvariable zu speichern.

Erstelle die Datei server/.env:

MONGODB_URI=mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/todoapp?retryWrites=true&w=majority
PORT=3001

Mongoose Model erstellen

Erstelle den Ordner server/models und die Datei Todo.js:

const mongoose = require('mongoose');

const todoSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, 'Titel ist erforderlich'],
    trim: true,
    minlength: [1, 'Titel darf nicht leer sein'],
    maxlength: [200, 'Titel darf maximal 200 Zeichen haben']
  },
  completed: {
    type: Boolean,
    default: false
  }
}, {
  timestamps: true  // Fügt createdAt und updatedAt automatisch hinzu
});

module.exports = mongoose.model('Todo', todoSchema);

Das Schema definiert:

  • title - Pflichtfeld, getrimmt, mit Längenvalidierung
  • completed - Boolean mit Standardwert false
  • timestamps - Automatische Zeitstempel für Erstellung und Änderung

Backend anpassen

Jetzt strukturieren wir das Backend besser mit einem Controller-Pattern und verbinden uns mit MongoDB.

Datenbankverbindung

Erstelle server/config/database.js:

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI);
    console.log(`MongoDB verbunden: ${conn.connection.host}`);
  } catch (error) {
    console.error('MongoDB Verbindungsfehler:', error.message);
    process.exit(1);
  }
};

module.exports = connectDB;

Controller erstellen

Erstelle server/controllers/todoController.js:

const Todo = require('../models/Todo');

// Alle Todos abrufen
exports.getAllTodos = async (req, res) => {
  try {
    const todos = await Todo.find().sort({ createdAt: -1 });
    res.json(todos);
  } catch (error) {
    res.status(500).json({ error: 'Fehler beim Laden der Todos' });
  }
};

// Neues Todo erstellen
exports.createTodo = async (req, res) => {
  try {
    const { title } = req.body;
    const todo = await Todo.create({ title });
    res.status(201).json(todo);
  } catch (error) {
    if (error.name === 'ValidationError') {
      const messages = Object.values(error.errors).map(e => e.message);
      return res.status(400).json({ error: messages.join(', ') });
    }
    res.status(500).json({ error: 'Fehler beim Erstellen' });
  }
};

// Todo aktualisieren
exports.updateTodo = async (req, res) => {
  try {
    const { id } = req.params;
    const { title, completed } = req.body;

    const todo = await Todo.findByIdAndUpdate(
      id,
      { title, completed },
      { new: true, runValidators: true }
    );

    if (!todo) {
      return res.status(404).json({ error: 'Todo nicht gefunden' });
    }

    res.json(todo);
  } catch (error) {
    if (error.name === 'ValidationError') {
      const messages = Object.values(error.errors).map(e => e.message);
      return res.status(400).json({ error: messages.join(', ') });
    }
    res.status(500).json({ error: 'Fehler beim Aktualisieren' });
  }
};

// Todo löschen
exports.deleteTodo = async (req, res) => {
  try {
    const { id } = req.params;
    const todo = await Todo.findByIdAndDelete(id);

    if (!todo) {
      return res.status(404).json({ error: 'Todo nicht gefunden' });
    }

    res.json(todo);
  } catch (error) {
    res.status(500).json({ error: 'Fehler beim Löschen' });
  }
};

Der Controller nutzt Mongoose-Methoden:

  • Todo.find() - Alle Dokumente finden
  • Todo.create() - Neues Dokument erstellen
  • Todo.findByIdAndUpdate() - Nach ID aktualisieren
  • Todo.findByIdAndDelete() - Nach ID löschen

Routes auslagern

Erstelle server/routes/todos.js:

const express = require('express');
const router = express.Router();
const todoController = require('../controllers/todoController');

router.get('/', todoController.getAllTodos);
router.post('/', todoController.createTodo);
router.put('/:id', todoController.updateTodo);
router.delete('/:id', todoController.deleteTodo);

module.exports = router;

Hauptdatei aktualisieren

Ersetze server/index.js:

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const connectDB = require('./config/database');
const todoRoutes = require('./routes/todos');

const app = express();
const PORT = process.env.PORT || 3001;

// Mit Datenbank verbinden
connectDB();

// Middleware
app.use(cors());
app.use(express.json());

// Routes
app.use('/api/todos', todoRoutes);

// Error Handling Middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Etwas ist schiefgelaufen!' });
});

// Server starten
app.listen(PORT, () => {
  console.log(`Server läuft auf http://localhost:${PORT}`);
});

Die neue Struktur ist übersichtlicher:

server/
├── config/
│   └── database.js      # Datenbankverbindung
├── controllers/
│   └── todoController.js # Geschäftslogik
├── models/
│   └── Todo.js          # Mongoose Schema
├── routes/
│   └── todos.js         # API Routes
├── index.js             # Entry Point
├── .env                 # Umgebungsvariablen
└── package.json

Frontend anpassen

Das Frontend braucht nur eine kleine Anpassung. MongoDB verwendet _id statt id als Primärschlüssel.

todoApi.js aktualisieren

Die API-Schicht bleibt gleich - Mongoose gibt standardmäßig auch id als virtuelles Feld zurück. Falls du Probleme hast, kannst du in den Komponenten todo._id statt todo.id verwenden.

TodoItem.jsx (optional)

Falls nötig, ändere die ID-Referenzen:

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo._id)}
      />
      <span className="todo-title">{todo.title}</span>
      <button
        className="delete-btn"
        onClick={() => onDelete(todo._id)}
        aria-label="Todo löschen"
      >
        ×
      </button>
    </li>
  );
}

Und in App.jsx:

const toggleTodo = async (id) => {
  try {
    const todo = todos.find(t => (t._id || t.id) === id);
    const updated = await todoApi.updateTodo(id, {
      completed: !todo.completed
    });
    setTodos(todos.map(t => (t._id || t.id) === id ? updated : t));
  } catch (err) {
    setError('Fehler beim Aktualisieren');
  }
};

Testen der Anwendung

Starte das Backend:

cd server
node index.js

Du solltest sehen:

Server läuft auf http://localhost:3001
MongoDB verbunden: cluster0.xxxxx.mongodb.net

Starte das Frontend:

cd client
npm run dev

Jetzt der entscheidende Test: Füge ein paar Todos hinzu, stoppe den Server mit Ctrl+C, starte ihn wieder - und deine Todos sind noch da!

Du kannst die Daten auch direkt in MongoDB Atlas unter "Browse Collections" sehen.

Vorteile der neuen Architektur

Durch die Umstellung haben wir mehrere Verbesserungen:

  1. Persistenz - Daten überleben Server-Neustarts
  2. Validierung - Mongoose validiert Daten vor dem Speichern
  3. Timestamps - Automatische Zeitstempel für Auditing
  4. Skalierbarkeit - MongoDB skaliert für große Datenmengen
  5. Bessere Struktur - Controller-Pattern trennt Logik sauber

Zusammenfassung

Du hast erfolgreich den MERN Stack implementiert:

  • MongoDB als NoSQL-Datenbank
  • Mongoose für Schema-Definition und Validierung
  • Controller-Pattern für bessere Code-Organisation
  • Umgebungsvariablen für sichere Konfiguration

Der MERN Stack ist ideal für:

  • Rapid Prototyping
  • Anwendungen mit flexiblen Datenstrukturen
  • Teams, die durchgängig JavaScript nutzen wollen

Im dritten Teil dieser Serie zeige ich dir, wie du stattdessen PostgreSQL verwendest - für Anwendungen, die relationale Daten und ACID-Transaktionen brauchen.


Weiterführende Links:

Möchtest du auch so einen Blog?

Ich entwickle moderne, SEO-optimierte Websites und Blogs mit Next.js, React und Tailwind CSS.