--- marp: true theme: gaia paginate: true backgroundColor: #fff header: "Web Engineering – DHBW Stuttgart" footer: "Michael Czechowski – SoSe 2026" --- # Kapitel 4 — Node.js Advanced ## async · Files · Database · Auth · Errors --- # Asynchron + Promises --- # Synchron vs. Asynchron **Synchron** (blockierend): ```javascript const data = readFileSync('huge.txt'); // wartet console.log('weiter'); // erst danach ``` **Asynchron** (non-blocking): ```javascript const data = await readFile('huge.txt'); // wartet ohne blockieren console.log('weiter'); // erst danach ``` Node's Stärke: **non-blocking I/O** für viele parallele Operationen. --- # Promise — Anatomie ```javascript const promise = fetch('https://api.example.com'); // then-style (alt) promise .then(res => res.json()) .then(data => console.log(data)) .catch(err => console.error(err)); // async/await (modern) try { const res = await fetch('https://api.example.com'); const data = await res.json(); console.log(data); } catch (err) { console.error(err); } ``` --- # Sequenziell vs. Parallel ```javascript // SEQUENZIELL — langsam (warten + warten + warten) const a = await fetch('/api/a'); const b = await fetch('/api/b'); const c = await fetch('/api/c'); // Gesamtzeit: a + b + c // PARALLEL — schnell (alle gleichzeitig starten) const [a, b, c] = await Promise.all([ fetch('/api/a'), fetch('/api/b'), fetch('/api/c'), ]); // Gesamtzeit: max(a, b, c) ``` --- # Promise-Methoden | Methode | Was | |---------|-----| | `Promise.all([…])` | alle erfolgreich ODER ein Fehler | | `Promise.allSettled([…])` | alle, Resultat = { status, value/reason } | | `Promise.race([…])` | erstes Resultat gewinnt (auch Fehler) | | `Promise.any([…])` | erstes erfolgreiches, ignore Fehler | ```javascript const fastest = await Promise.race([ fetch('/api'), new Promise((_, rej) => setTimeout(() => rej('timeout'), 3000)) ]); ``` --- # Filesystem --- # fs/promises — Files lesen + schreiben ```javascript import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises'; // Lesen const text = await readFile('input.txt', 'utf-8'); const buf = await readFile('image.png'); // Buffer // Schreiben await writeFile('output.txt', 'Hallo Welt'); await writeFile('data.json', JSON.stringify({ x: 1 }, null, 2)); // Verzeichnis lesen const files = await readdir('./uploads'); // Verzeichnis anlegen await mkdir('./tmp', { recursive: true }); ``` --- # Streams für große Dateien ```javascript import { createReadStream, createWriteStream } from 'node:fs'; import { pipeline } from 'node:stream/promises'; // 10 GB Logfile ohne RAM-Explosion verarbeiten await pipeline( createReadStream('huge.log'), // ... transform-stream ... createWriteStream('output.log') ); ``` Streams = Daten **stückweise** verarbeiten, nicht alles in RAM. --- # Datenbank --- # Welche DB für DHBW-Projekte? | DB | Wann | |----|------| | **SQLite** | klein, embedded, eine Datei. Lokal/dev/demo. | | **PostgreSQL** | Standard für „echte" Apps. Open Source, mächtig. | | **MongoDB** | NoSQL, dokumentenorientiert. Selten erste Wahl. | | **Redis** | Cache, Queue, schnell. | Für Prüfungsleistung: **SQLite** (kein Setup) oder **Postgres** (production-realistisch). --- # SQLite mit `better-sqlite3` ```javascript import Database from 'better-sqlite3'; const db = new Database('app.db'); db.prepare(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `).run(); // Insert const insert = db.prepare('INSERT INTO users (name) VALUES (?)'); insert.run('Ada'); // Query const users = db.prepare('SELECT * FROM users').all(); const ada = db.prepare('SELECT * FROM users WHERE id = ?').get(1); ``` --- # ORM / Query-Builder Für komplexere Apps: ORM statt Raw-SQL. | Tool | Typ | |------|-----| | **Drizzle** | TypeScript-first, modern, lightweight | | **Prisma** | umfangreich, eigenes Schema | | **Knex** | Query-Builder, kein ORM | | **TypeORM** | klassisches ORM | ```typescript // Drizzle Beispiel const result = await db.select().from(users).where(eq(users.id, 1)); ``` --- # CRUD-Pattern mit Express + SQLite ```javascript app.get('/users', (req, res) => { const users = db.prepare('SELECT * FROM users').all(); res.json(users); }); app.get('/users/:id', (req, res) => { const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id); user ? res.json(user) : res.sendStatus(404); }); app.post('/users', (req, res) => { const { name } = req.body; const result = db.prepare('INSERT INTO users (name) VALUES (?)').run(name); res.status(201).json({ id: result.lastInsertRowid, name }); }); ``` --- # Authentifizierung --- # Auth-Strategien | Strategie | Wann | |-----------|------| | **Session-Cookie** | klassische Web-App, Server-Side Render | | **JWT** (JSON Web Token) | API + SPA, stateless | | **OAuth** | „Mit Google einloggen", Drittpartei-Login | | **Magic Link** | Email statt Passwort, modern | | **Passkeys** | passwordless, FIDO2, Zukunft | Für DHBW-Projekt: **Session-Cookie** (express-session) ODER **JWT**. --- # Passwörter — *niemals plaintext* ```javascript import bcrypt from 'bcrypt'; // Registrieren const hashed = await bcrypt.hash('mein-passwort', 12); db.prepare('INSERT INTO users (email, password) VALUES (?, ?)').run(email, hashed); // Login const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email); const valid = await bcrypt.compare(password, user.password); ``` **bcrypt** oder **argon2** für Passwort-Hashing. Niemals MD5/SHA1. --- # JWT — kurzer Überblick ```javascript import jwt from 'jsonwebtoken'; // Signieren (beim Login) const token = jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1d' } ); res.cookie('token', token, { httpOnly: true, secure: true }); // Verifizieren (Middleware) const token = req.cookies.token; const payload = jwt.verify(token, process.env.JWT_SECRET); req.user = payload; ``` `httpOnly + secure + sameSite` — XSS/CSRF-Schutz. --- # Error-Handling --- # try/catch + Error-Klassen ```javascript class NotFoundError extends Error { constructor(message) { super(message); this.name = 'NotFoundError'; this.status = 404; } } try { const user = await getUser(id); if (!user) throw new NotFoundError(`User ${id} not found`); res.json(user); } catch (err) { if (err instanceof NotFoundError) return res.status(404).json({ error: err.message }); console.error(err); res.sendStatus(500); } ``` --- # Globaler Express-Error-Handler ```javascript // Am Ende aller Routes! app.use((err, req, res, next) => { console.error(err); if (err.status) { return res.status(err.status).json({ error: err.message }); } res.status(500).json({ error: 'Internal Server Error' }); }); ``` 4 Argumente: `(err, req, res, next)` → Express erkennt Error-Handler. --- # Selbstlernen — REST-API mit DB Erweitert eure Kap-3-API: 1. **SQLite-DB** mit `better-sqlite3` 2. **CRUD** mit DB statt In-Memory-Array 3. **Validierung** (z.B. mit `zod`) für POST-Body 4. **Auth:** einfache Login-Route mit bcrypt + JWT 5. **Global Error-Handler** für saubere Responses **Bonus:** Async-Race-Condition simulieren + Promise.all nutzen. --- # Zusammenfassung **Heute gelernt:** - **async/await + Promise** sauber anwenden, sequenziell vs. parallel - **fs/promises + Streams** für Dateien - **SQLite + Prepared Statements** als pragmatische DB - **bcrypt** für Passwörter, **JWT** als Token-Pattern - **Error-Klassen + Globaler Handler** in Express **Nächste Stunde:** Testing — Vitest, TDD, Integration, E2E.