Zum Inhalt springen
Kontakt

Sebastian Nawrot
Dorneystr. 45
44149 Dortmund

Webentwicklung & Technik

React Todo-App mit PostgreSQL: SQL-Datenbank anbinden

Verbinde deine React Todo-App mit PostgreSQL oder MySQL. Lerne SQL-Datenbankintegration mit Sequelize ORM.

Sebastian Nawrot
7 Min. Lesezeit
#React#PostgreSQL#MySQL#Sequelize#SQL#Node.js#Tutorial
React Todo-App mit PostgreSQL: SQL-Datenbank anbinden

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?

KriteriumMongoDB (NoSQL)PostgreSQL/MySQL (SQL)
DatenstrukturFlexibel, dokumentbasiertStreng, tabellenbasiert
SchemaSchema-on-readSchema-on-write
BeziehungenEingebettete DokumenteForeign Keys, JOINs
TransaktionenBegrenzt (verbessert sich)ACID-konform
SkalierungHorizontalPrimär vertikal
Ideal fürPrototyping, flexible DatenBusiness-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:

MongooseSequelize
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?

PostgreSQLMySQL
Komplexe Queries, Window FunctionsEinfachere Anwendungen
JSON-Spalten, ArraysWeite Verbreitung, viel Hosting
Bessere Standards-KonformitätSchnell für Lese-Operationen
Open Source, keine LizenzfragenTeil 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:

  1. Authentifizierung - JWT, Sessions, OAuth
  2. Deployment - Docker, Cloud-Hosting
  3. Testing - Unit Tests, Integration Tests
  4. Migrationen - Datenbankänderungen versionieren
  5. Caching - Redis für Performance

Kompletter Code: Alle drei Versionen findest du auf GitHub.

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.