====== LU17 – CRUD mit Express & MySQL ====== ===== Lernziele ===== * Sie können Ihre **MySQL-Datenbank** aus einem Express-Server heraus ansprechen. * Sie können die CRUD-Routen aus LU16 (''GET'', ''POST'', ''PUT'', ''DELETE'') so erweitern, dass sie mit einer **echten Tabelle ''posts''** arbeiten. * Sie setzen einfache **Validierung** ein (Pflichtfelder prüfen). * Sie verwenden passende **HTTP-Statuscodes** (200, 201, 400, 404, 500). * Sie testen alle Operationen mit **Postman**. ===== Rückblick auf LU16 ===== In LU16 haben Sie eine API für Social-Media-Posts gebaut – aber noch ohne Datenbank: * Daten waren in einer **JavaScript-Liste ''posts''** gespeichert (In-Memory). * Ihre Routen waren z.B.: * ''GET /api/posts'' * ''GET /api/posts/:id'' * ''POST /api/posts'' In LU17 ersetzen Sie diese In-Memory-Liste durch eine echte Datenbank: * Die Daten kommen aus der **MySQL-Tabelle ''posts''**. * Ihr Express-Server ist die **Brücke** zwischen Client (Postman) und Datenbank. * Die gleiche Idee bleibt: **HTTP-Anfrage → Route → Logik/SQL → HTTP-Antwort**. {{:modul:m290_guko:learningunits:lu16:theorie:client_server_http_req_response_kitchen.png?direct&900|}} ===== HTTP-Statuscodes ===== Wenn Sie mit Postman testen, sehen Sie immer einen Statuscode. Dieser Code ist Ihr „Kurzfazit“, ob der Request geklappt hat. {{ :modul:m290_guko:learningunits:lu17:theorie:http_statuscodes.png?direct&1100 | HTTP-Statuscodes }} ^ Situation ^ Typischer Statuscode ^ Bedeutung ^ | Erfolgreich Daten gelesen (z.B. GET) | **200** | OK (Daten kommen zurück) | | Erfolgreich Datensatz erstellt (POST) | **201** | Created (Neuer Datensatz wurde erstellt) | | Erfolgreich aktualisiert (PUT) | **200** | OK (Update hat geklappt) | | Erfolgreich gelöscht (DELETE) | **200** | OK (Löschen hat geklappt; Sie können zur Kontrolle z.B. das gelöschte Objekt oder eine Message zurückgeben) | | Pflichtfelder fehlen / ungültige Eingabe | **400** | Bad Request (Client hat falsche/fehlende Daten geschickt) | | ID existiert nicht | **404** | Not Found (Datensatz wurde nicht gefunden) | | Datenbank-/Serverfehler | **500** | Internal Server Error (Problem auf Server/DB-Seite) | ===== Vorbereitung: MySQL-Datenbank "social_media" ===== Für die Verbindung mit MySQL benötigen wir eine bestehende Datenbank. Wir arbeiten mit der Datenbank "social_media", welche die Tabellen users und posts beinhaltet. Laden Sie hier den SQL-Dump herunter und lassen Sie das Skript in Webstorm laufen via //Run SQL Script...// (eine Anleitung, wie das prinzipiell geht finden Sie in LU09). {{ :modul:m290_guko:learningunits:lu17:theorie:social_media_db-dump.sql.zip |SQL-Dump einer fiktiven Social-Media-Datenbank}} Das ERD der Datenbank sieht so aus: {{ :modul:m290_guko:learningunits:lu17:theorie:social_media_crowsfoot_ids_updated.drawio.png?direct&900 | Crow's Foot Diagramm Social Media DB}} Eine mögliche Tabellenstruktur für die Tabelle "posts": ^ Spalte ^ Datentyp ^ Beschreibung ^ | ''id'' | INT, PRIMARY KEY, AUTO_INCREMENT | Primärschlüssel | | ''user_id'' | INT | Referenz auf User (FK) | | ''title'' | VARCHAR(255) | Titel des Posts | | ''image_url'' | VARCHAR(512) | Bild-URL | | ''description'' | TEXT | Beschreibung | | ''likes'' | INT | Anzahl Likes | Achtung: Die Primär-Spalte heisst neu ''id'' und nicht mehr ''post_id'' wie in LU16. ===== Schritt 1: mysql2 installieren ===== Damit Node.js mit MySQL sprechen kann, verwenden wir ''mysql2''. npm install mysql2 ===== Schritt 2: connect.js erstellen (DB-Verbindung auslagern) ===== Damit ''index.js'' übersichtlich bleibt, erstellen Sie eine eigene Datei ''connect.js''. Diese Datei erstellt **eine Verbindung** und exportiert sie, damit Sie sie in Ihren Routes mit ''db.query(...)'' verwenden können. import mysql from 'mysql2'; // Hinweis: Verwenden Sie hier Ihren AppUser (nicht root). // Port 3306 ist der Standard-Port für MySQL auf localhost. const db = mysql.createConnection({ host: 'localhost', user: 'app_user', password: 'YOUR_PASSWORD_HERE', port: 3306, database: 'social_media', }); // connect() öffnet die Verbindung und gibt bei Erfolg/Fehler eine Meldung aus. db.connect((err) => { if (err) { console.error('DB-Verbindung fehlgeschlagen:', err); return; } console.log('Mit MySQL verbunden'); }); export { db }; **Was passiert hier?** * ''db'' ist die offene Verbindung und kann in ''index.js'' importiert werden (z.B. ''import { db } from "./connect.js";''). * In der LB03 sollen Sie **nicht** als ''root'' arbeiten → verwenden Sie einen AppUser. ===== Schritt 3: index.js vorbereiten (Express + DB verwenden) ===== Sie bauen auf dem Setup aus LU16 auf: * ''app.use(express.json())'' bleibt wichtig (für ''req.body''). * Sie importieren ''db'' aus ''connect.js''. Die Verbindung wird in ''connect.js'' bereits aufgebaut (''db.connect(...)''). * __Löschen__ Sie die Test-Tabelle aus LU16 ''let posts = [{ post_id: 1, ... }];'' import express from 'express'; import { db } from './connect.js'; const app = express(); const port = 3000; app.use(express.json()); // Test-Route: schnell prüfen, ob der Server läuft app.get('/', (req, res) => { res.send('API mit Datenbank ist online'); }); // Optional: kleine Test-Route für DB (zeigt Serverzeit aus MySQL) app.get('/api/db-test', (req, res) => { db.query('SELECT NOW() AS server_time', (err, results) => { if (err) { console.error('DB-Fehler bei /api/db-test:', err); return res.status(500).send('DB-Test fehlgeschlagen'); } res.status(200).json(results[0]); }); }); // Server starten app.listen(port, () => { console.log(`API läuft auf http://localhost:${port}`); }); **Was passiert hier?** * ''db'' kommt aus ''connect.js'' und ist die MySQL-Verbindung. * Ihre Routen können direkt ''db.query(...)'' verwenden. * Mit ''/api/db-test'' prüfen Sie schnell, ob die DB erreichbar ist (Status ''200'' + JSON), ohne schon CRUD zu programmieren. **Server starten** Nicht alle Lernenden haben ''nodemon'' installiert. Beides ist möglich: node index.js oder (falls Sie ein dev-Script mit nodemon haben): npm run dev ===== CRUD-Routen: posts jetzt mit echter MySQL-Tabelle ===== Ab hier ersetzen Sie die In-Memory-Liste aus LU16 durch SQL. **Konvention:** Wir bleiben wie in LU16 bei ''/api/posts'' (statt nur ''/posts''), damit Ihre API klar erkennbar ist. ==== READ: alle Posts (GET /api/posts) ==== // READ – alle Posts aus der DB // Route: GET http://localhost:3000/api/posts app.get('/api/posts', (req, res) => { // SQL-Abfrage: alle Spalten, die wir zurückgeben möchten const sql = ` SELECT id, user_id, title, image_url, description, likes FROM posts `; // db.query(...) führt die SQL-Abfrage aus db.query(sql, (err, results) => { // Falls die DB einen Fehler liefert -> 500 if (err) { console.error('DB-Fehler bei GET /api/posts:', err); return res.status(500).send('Serverfehler bei der Post-Abfrage'); } // results ist ein Array mit Zeilen (Rows) res.status(200).json(results); }); }); === Test in Postman === * Methode: ''GET'' * URL: ''http://localhost:3000/api/posts'' * Erwartung: ''200'' + JSON-Liste ==== READ: einzelner Post (GET /api/posts/:id) ==== // READ – einzelner Post nach ID aus der DB // Route: GET http://localhost:3000/api/posts/1 app.get('/api/posts/:id', (req, res) => { // Route-Parameter :id kommt immer als String -> Number(...) macht eine Zahl daraus const id = Number(req.params.id); // Einfache Validierung: ist id überhaupt eine Zahl? if (Number.isNaN(id)) { return res.status(400).send('Ungültige ID (muss eine Zahl sein)'); } const sql = ` SELECT id, user_id, title, image_url, description, likes FROM posts WHERE id = ? `; // Platzhalter ? wird durch id ersetzt -> Schutz vor SQL-Injection db.query(sql, [id], (err, results) => { if (err) { console.error('DB-Fehler bei GET /api/posts/:id:', err); return res.status(500).send('Serverfehler bei der Post-Abfrage'); } // Wenn keine Zeile gefunden -> 404 if (results.length === 0) { return res.status(404).send('Post nicht gefunden'); } // results[0] ist der erste (und hier einzige) Treffer res.status(200).json(results[0]); }); }); ==== CREATE: neuen Post erstellen (POST /api/posts) ==== // CREATE – neuen Post in der DB anlegen // Route: POST http://localhost:3000/api/posts app.post('/api/posts', (req, res) => { // Daten kommen aus dem Request-Body (Postman: Body -> raw -> JSON) const user_id = req.body.user_id; const title = req.body.title; const image_url = req.body.image_url; const description = req.body.description; // Validierung: Pflichtfelder if (user_id === undefined || user_id === null || title === undefined || title === null || title === '') { return res.status(400).send('Bitte mindestens user_id und title angeben.'); } const sql = ` INSERT INTO posts (user_id, title, image_url, description, likes) VALUES (?, ?, ?, ?, 0) `; const values = [ user_id, title, image_url || '', description || '' ]; // INSERT ausführen db.query(sql, values, (err, result) => { if (err) { console.error('DB-Fehler bei POST /api/posts:', err); return res.status(500).send('Serverfehler beim Erstellen des Posts'); } // insertId kommt von MySQL AUTO_INCREMENT const newPost = { id: result.insertId, user_id: user_id, title: title, image_url: image_url || '', description: description || '', likes: 0 }; res.status(201).json(newPost); }); }); === Test in Postman === **Test-Body (Postman → Body → raw → JSON)** { "user_id": 1, "title": "Neuer DB-Post", "image_url": "https://example.com/post.jpg", "description": "Dieser Post wurde in MySQL gespeichert." } ==== UPDATE: Post ändern (PUT /api/posts/:id) ==== In LU16b haben Sie einen Post so aktualisiert: Nur die Felder, die im Body wirklich mitgeschickt werden, werden geändert. Genau diese Logik übernehmen wir jetzt – der Unterschied ist nur: statt im Array ändern wir jetzt die MySQL-Tabelle ''posts''. // UPDATE – Post vollständig ersetzen (PUT) // Route: PUT http://localhost:3000/api/posts/1 app.put('/api/posts/:id', (req, res) => { const id = Number(req.params.id); if (Number.isNaN(id)) { return res.status(400).send('Ungültige ID (muss eine Zahl sein)'); } // Alle Felder werden erwartet (PUT ersetzt alles) const user_id = req.body.user_id; const title = req.body.title; const image_url = req.body.image_url; const description = req.body.description; const likes = req.body.likes; // Validierung: fehlen Felder? // likes kann 0 sein -> deshalb auf undefined prüfen if ( user_id === undefined || title === undefined || image_url === undefined || description === undefined || likes === undefined ) { return res.status(400).send('Bitte user_id, title, image_url, description und likes mitsenden (PUT ersetzt alles).'); } const sql = ` UPDATE posts SET user_id = ?, title = ?, image_url = ?, description = ?, likes = ? WHERE id = ? `; const values = [user_id, title, image_url, description, likes, id]; db.query(sql, values, (err, result) => { if (err) { console.error('DB-Fehler bei PUT /api/posts/:id:', err); return res.status(500).send('Serverfehler beim Aktualisieren des Posts'); } if (result.affectedRows === 0) { return res.status(404).send('Post nicht gefunden'); } // 200 OK + das "neue" Objekt zurückgeben res.status(200).json({ id: id, user_id: user_id, title: title, image_url: image_url, description: description, likes: likes }); }); }); === Test in Postman === **Test-Body** { "user_id": 1, "title": "Titel (replaced)", "image_url": "https://example.com/new.jpg", "description": "Dieser Post wurde komplett ersetzt.", "likes": 5 } Erwartung: * Status ''200 OK'' * JSON-Objekt des aktualisierten Posts (inkl. neuem ''title'' und ''likes'') ==== DELETE: Post löschen (DELETE /api/posts/:id) ==== // DELETE – Post löschen app.delete('/api/posts/:id', (req, res) => { const id = Number(req.params.id); if (Number.isNaN(id)) { return res.status(400).send('Ungültige ID (muss eine Zahl sein)'); } const sql = 'DELETE FROM posts WHERE id = ?'; db.query(sql, [id], (err, result) => { if (err) { console.error('DB-Fehler bei DELETE /api/posts/:id:', err); return res.status(500).send('Serverfehler beim Löschen des Posts'); } if (result.affectedRows === 0) { return res.status(404).send('Post nicht gefunden'); } // Antwort: 200 + Message res.status(200).json({ message: `Post mit id=${id} wurde gelöscht.` }); }); }); === Test mit Postman === * Methode: ''DELETE'' * URL: ''http://localhost:3000/api/posts/1'' Erwartung: * Status ''200 OK'' * JSON-Objekt des gelöschten Posts * Danach ''GET /api/posts/1'' → ''404 Not Found'' ===== Typische Fehlerquellen (aus LU16, jetzt noch wichtiger) ===== * ''req.params.id'' ist immer ein String → mit ''Number(...)'' umwandeln (und ''Number.isNaN(...)'' prüfen). * Ohne ''app.use(express.json())'' ist ''req.body'' leer. * SQL immer mit Platzhaltern ''?'' schreiben (Prepared Statements). * Denken Sie bei ''db.query(...)'' immer an die drei Fälle: * Eingaben ok? → sonst ''400'' * Datensatz gefunden? → sonst ''404'' (''results.length === 0'' oder ''affectedRows === 0'') * DB-Fehler? → ''500'' ===== Transfer auf Ihr Projekt (LB03) ===== Für Ihr Projekt ersetzen Sie ''posts'' durch Ihre eigenen Tabellen (z.B. ''serie'', ''actor'', ''serie_actor''): * Ressourcen-Route: z.B. ''/api/serien'' * CRUD: ''GET'', ''POST'', ''PUT'', ''DELETE'' * Zusätzlich: * mindestens **eine JOIN-Route** (z.B. Serien inkl. Schauspieler:innen) * mindestens **eine Aggregat-Route** (z.B. Durchschnittsbewertung pro Genre)