per user-request einen branch der main-content UND rewrite-content trägt — saubere separierung statt symlink/merge-conflict. struktur: - slides/<course>/ → main-content restored, eigene assets (kein new-demo-leak) - slides/<course>-2026/ → rewrite-content, eigene assets (komplett kopiert + new demos) Makefile: COURSES += 223015b-2026 223015c-2026 dhbw-2026 mit eigenen NAME/KAPITEL/DEPLOY/KLAUSUR-konfig. Klausur für b+c=1, dhbw=leer. Deploy-paths /hdm/223015b-2026 etc. make build durchlaufen für alle 6 decks, 0 fehler. dedup: 223015c+dhbw assets identisch in beiden ordnern (kein neu-demos auf b ergänzt). 223015b 86 → 120 files in -2026 (= +34 neue eroeffner/usb-c-drei-achsen/container-vs-codec/etc).
381 lines
8.3 KiB
Markdown
381 lines
8.3 KiB
Markdown
---
|
||
marp: true
|
||
theme: gaia
|
||
paginate: true
|
||
backgroundColor: #fff
|
||
header: "Web Engineering – DHBW Stuttgart"
|
||
footer: "Michael Czechowski – SoSe 2026"
|
||
---
|
||
|
||
<style>
|
||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; --color-dimmed: #4a4a6a; }
|
||
section.invert { --color-foreground: #fff; }
|
||
section { font-size: 1.4rem; }
|
||
h1 { color: #a02060; }
|
||
section.invert h1 { color: #fff; }
|
||
h2 { color: #1f2937; }
|
||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||
pre code { background: transparent; color: inherit; }
|
||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||
a { color: var(--color-highlight); }
|
||
</style>
|
||
|
||
<!-- _class: lead -->
|
||
|
||
# Kapitel 4 — Node.js Advanced
|
||
## async · Files · Database · Auth · Errors
|
||
|
||
---
|
||
|
||
<!-- _class: lead -->
|
||
|
||
# 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))
|
||
]);
|
||
```
|
||
|
||
---
|
||
|
||
<!-- _class: lead -->
|
||
|
||
# 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.
|
||
|
||
---
|
||
|
||
<!-- _class: lead -->
|
||
|
||
# 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 });
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
<!-- _class: lead -->
|
||
|
||
# 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.
|
||
|
||
---
|
||
|
||
<!-- _class: lead -->
|
||
|
||
# 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.
|
||
|
||
---
|
||
|
||
<!-- _class: aufgabe -->
|
||
|
||
# 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.
|