In den ersten beiden Teilen dieser Serie haben wir eine Todo-App mit In-Memory Datastore und MongoDB gebaut. Jetzt geht es an die SQL-Datenbanken: PostgreSQL und MySQL.
SQL-Datenbanken sind der Industriestandard für Anwendungen, die relationale Daten, Transaktionen und komplexe Abfragen brauchen. Mit Sequelize als ORM bleibt der Code fast identisch zu unserer MongoDB-Version.
SQL vs. NoSQL - Wann was?
| Kriterium | MongoDB (NoSQL) | PostgreSQL/MySQL (SQL) |
|---|---|---|
| Datenstruktur | Flexibel, dokumentbasiert | Streng, tabellenbasiert |
| Schema | Schema-on-read | Schema-on-write |
| Beziehungen | Eingebettete Dokumente | Foreign Keys, JOINs |
| Transaktionen | Begrenzt (verbessert sich) | ACID-konform |
| Skalierung | Horizontal | Primär vertikal |
| Ideal für | Prototyping, flexible Daten | Business-Logik, Finanzen |
Für unsere Todo-App macht beides Sinn. Aber sobald du User-Accounts, Kategorien oder Sharing-Features brauchst, spielen relationale Datenbanken ihre Stärken aus.
PostgreSQL Setup
Option 1: Docker (empfohlen)
Der schnellste Weg ohne lokale Installation:
docker run --name todo-postgres \
-e POSTGRES_USER=todouser \
-e POSTGRES_PASSWORD=todopass \
-e POSTGRES_DB=todoapp \
-p 5432:5432 \
-d postgres:16
Option 2: Lokale Installation
macOS:
brew install postgresql@16
brew services start postgresql@16
createdb todoapp
Ubuntu/Debian:
sudo apt install postgresql postgresql-contrib
sudo -u postgres createdb todoapp
sudo -u postgres createuser todouser --pwprompt
Verbindung testen
Mit psql:
psql -h localhost -U todouser -d todoapp
Oder mit einem GUI-Tool wie pgAdmin oder DBeaver.
Sequelize ORM einrichten
Sequelize ist ein mächtiges ORM für Node.js, das PostgreSQL, MySQL, SQLite und mehr unterstützt.
cd server
npm install sequelize pg pg-hstore
Für MySQL wäre es:
npm install sequelize mysql2
Erstelle/aktualisiere die .env:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=todoapp
DB_USER=todouser
DB_PASSWORD=todopass
DB_DIALECT=postgres
PORT=3001
Datenbankverbindung konfigurieren
Erstelle server/config/database.js:
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT, // 'postgres' oder 'mysql'
logging: false, // SQL-Queries in Konsole ausblenden
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
}
);
const connectDB = async () => {
try {
await sequelize.authenticate();
console.log(`${process.env.DB_DIALECT} verbunden: ${process.env.DB_HOST}`);
// Tabellen synchronisieren (in Produktion: Migrationen verwenden)
await sequelize.sync({ alter: true });
console.log('Datenbank synchronisiert');
} catch (error) {
console.error('Datenbankfehler:', error.message);
process.exit(1);
}
};
module.exports = { sequelize, connectDB };
Todo Model mit Sequelize
Ersetze server/models/Todo.js:
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const Todo = sequelize.define('Todo', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: {
type: DataTypes.STRING(200),
allowNull: false,
validate: {
notEmpty: {
msg: 'Titel darf nicht leer sein'
},
len: {
args: [1, 200],
msg: 'Titel muss zwischen 1 und 200 Zeichen haben'
}
}
},
completed: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
}, {
tableName: 'todos',
timestamps: true, // createdAt, updatedAt
underscored: true // snake_case für Spalten
});
module.exports = Todo;
Sequelize erstellt automatisch diese Tabelle:
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE
);
Controller anpassen
Ersetze server/controllers/todoController.js:
const Todo = require('../models/Todo');
// Alle Todos abrufen
exports.getAllTodos = async (req, res) => {
try {
const todos = await Todo.findAll({
order: [['createdAt', 'DESC']]
});
res.json(todos);
} catch (error) {
console.error(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 === 'SequelizeValidationError') {
const messages = error.errors.map(e => e.message);
return res.status(400).json({ error: messages.join(', ') });
}
console.error(error);
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.findByPk(id);
if (!todo) {
return res.status(404).json({ error: 'Todo nicht gefunden' });
}
// Nur übergebene Felder aktualisieren
if (title !== undefined) todo.title = title;
if (completed !== undefined) todo.completed = completed;
await todo.save();
res.json(todo);
} catch (error) {
if (error.name === 'SequelizeValidationError') {
const messages = error.errors.map(e => e.message);
return res.status(400).json({ error: messages.join(', ') });
}
console.error(error);
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.findByPk(id);
if (!todo) {
return res.status(404).json({ error: 'Todo nicht gefunden' });
}
await todo.destroy();
res.json(todo);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Fehler beim Löschen' });
}
};
Die Sequelize-Methoden sind sehr ähnlich zu Mongoose:
| Mongoose | Sequelize |
|---|---|
Todo.find() | Todo.findAll() |
Todo.findById(id) | Todo.findByPk(id) |
Todo.create() | Todo.create() |
todo.save() | todo.save() |
Todo.findByIdAndDelete() | todo.destroy() |
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
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Etwas ist schiefgelaufen!' });
});
app.listen(PORT, () => {
console.log(`Server läuft auf http://localhost:${PORT}`);
});
MySQL als Alternative
Der Wechsel zu MySQL ist trivial. Ändere nur die .env:
DB_HOST=localhost
DB_PORT=3306
DB_NAME=todoapp
DB_USER=root
DB_PASSWORD=password
DB_DIALECT=mysql
PORT=3001
Und installiere den MySQL-Treiber:
npm install mysql2
Der restliche Code bleibt identisch - das ist die Stärke von ORMs wie Sequelize.
Wann MySQL, wann PostgreSQL?
| PostgreSQL | MySQL |
|---|---|
| Komplexe Queries, Window Functions | Einfachere Anwendungen |
| JSON-Spalten, Arrays | Weite Verbreitung, viel Hosting |
| Bessere Standards-Konformität | Schnell für Lese-Operationen |
| Open Source, keine Lizenzfragen | Teil des LAMP-Stacks |
Für neue Projekte empfehle ich PostgreSQL - es ist mächtiger und hat keine Nachteile.
Frontend: Keine Änderung nötig
Das Beste an sauberer API-Architektur: Das Frontend merkt nichts von der Datenbankänderung. Die REST API bleibt identisch.
Der einzige Unterschied: Sequelize gibt id (Integer) zurück, MongoDB _id (ObjectId String). Falls du im Frontend _id verwendet hast, ändere es zurück zu id.
Vergleich der drei Ansätze
Nach drei Teilen haben wir dieselbe App mit drei verschiedenen Datenspeichern gebaut:
In-Memory Array
let todos = [];
todos.push(newTodo);
todos.filter(t => t.id !== id);
Pro: Einfach, keine Dependencies, perfekt zum Lernen Contra: Keine Persistenz, nicht skalierbar
MongoDB mit Mongoose
await Todo.create({ title });
await Todo.find().sort({ createdAt: -1 });
await Todo.findByIdAndDelete(id);
Pro: Flexibel, JavaScript-nativ, schnelle Entwicklung Contra: Keine echten Transaktionen, weniger SQL-Features
PostgreSQL mit Sequelize
await Todo.create({ title });
await Todo.findAll({ order: [['createdAt', 'DESC']] });
await todo.destroy();
Pro: ACID, relationale Daten, Industriestandard Contra: Rigides Schema, mehr Setup
Fazit und nächste Schritte
Du hast jetzt drei verschiedene Wege kennengelernt, Daten in einer React-App zu speichern. Welchen du wählst, hängt von deinem Projekt ab:
- Prototyp oder MVP: In-Memory oder MongoDB
- Startup mit flexiblen Anforderungen: MongoDB
- Enterprise oder komplexe Beziehungen: PostgreSQL
Weiterführende Themen
Diese Tutorial-Serie deckt die Grundlagen ab. Für produktionsreife Apps solltest du noch lernen:
- Authentifizierung - JWT, Sessions, OAuth
- Deployment - Docker, Cloud-Hosting
- Testing - Unit Tests, Integration Tests
- Migrationen - Datenbankänderungen versionieren
- Caching - Redis für Performance
Kompletter Code: Alle drei Versionen findest du auf GitHub.
Weiterführende Links:

