M295 Block 02 - Dokumentation¶
Block 02¶
Übersicht
In diesem Block lernen wir, wie man einen Webserver mit Node.js erstellt, APIs entwickelt und HTML-Templates verwendet.
Inhalt¶
WebServer mit Routing¶
2.3 Aufgabe: Routing¶
Ziel
Einen Webserver mit einem Basis-Routing erstellen
Aufgabe: Erstellen Sie einen Webserver auf Ihrem eigenen Raspi Docker Zugang. Dieser Webserver sollte folgendes Routing zulassen:
ueberblick→ Hier sollte ein HTML H1 zurückgegeben werden mit folgendem Inhalt: ueberblickprodukte→ Hier sollte ein HTML H2 zurückgegeben werden mit folgendem Inhalt: hier sehen Sie unsere Produkte
Erklärung¶
Der Webserver verwendet Node.js und das http-Modul, um HTTP-Anfragen zu verarbeiten. Je nach URL-Pfad wird eine andere Antwort zurückgesendet. Das Routing funktioniert über eine einfache if-else-Struktur, die den Request-URL überprüft.
Code¶
const http = require("http");
const fs = require("fs");
const port = Number(process.env.PORT || 4008);
http.createServer((req, res) => {
const pathName = req.url;
if (pathName === '/' || pathName === '/ueberblick') {
res.writeHead(200, {"Content-Type": "text/html; charset=utf-8"});
res.end('<h1>ueberblick</h1>');
} else if (pathName === '/produkte') {
res.writeHead(200, {"Content-Type": "text/html; charset=utf-8"});
res.end('<h2>hier sehen Sie unsere Produkte</h2>');
} else {
res.writeHead(404, {"Content-Type": "text/html; charset=utf-8"});
res.end('<h1>Seite nicht gefunden</h1>');
}
}).listen(port, () => console.log("Server läuft im Container auf Port", port));
Code-Erklärung
- Zeile 5: Wir extrahieren den URL-Pfad aus der Anfrage
- Zeile 7-9: Bei
/oder/ueberblickwird ein H1-Element mit "ueberblick" zurückgegeben - Zeile 10-12: Bei
/produktewird ein H2-Element mit dem entsprechenden Text zurückgegeben - Zeile 13-16: Für alle anderen Pfade wird eine 404-Fehlerseite angezeigt
Start des Servers¶
Zuerst muss man ins richtige Verzeichnis wechseln, wo die server.js liegt:
Ausgabe im Terminal¶
[+] up 2/2
✓ Network lars_default Created
✓ Container lars-app-1 Created
Attaching to app-1
app-1 | Server läuft im Container auf Port 4008
Ergebnis im Browser¶
Bei Aufruf von http://10.27.160.12:4008/ueberblick:
Darstellung im Browser:
(als große Überschrift)Bei Aufruf von http://10.27.160.12:4008/produkte:
Darstellung im Browser:
(als Überschrift Ebene 2)Routing erfolgreich implementiert
Der Webserver reagiert nun korrekt auf verschiedene URL-Pfade und gibt jeweils die entsprechende HTML-Antwort zurück.
API Erstellen¶
2.4 Erstellung unserer ersten API¶
Ziel
Daten aus einer JSON-Datei lesen, parsen und über eine API zur Verfügung stellen
Eine API stellt Daten zur Verfügung. Wir schauen uns an, wie man Daten aus einer JSON-Datei ausliest und über einen Webserver bereitstellt.
Was ist JSON?¶
JSON (JavaScript Object Notation) ist ein Datenaustauschformat, das ähnlich wie JavaScript-Objekte aussieht, aber ein standardisiertes Format zum Speichern und Übertragen von Daten ist.
Schritt 1: JSON-Daten einlesen¶
Wir lesen die Datei data.json ein, die sich im Ordner dev-data befindet. Diese Daten werden von unserem Server zurückgesendet, wenn sie angefragt werden.
const data = fs.readFileSync(`${__dirname}/dev-data/data.json`, 'utf-8');
const dataObj = JSON.parse(data);
Wichtige Konzepte
__dirnamebezeichnet den Pfad, an welchem gerade unsereindex.jsliegtJSON.parse()wandelt JSON-Daten in JavaScript Code umreadFileSync()liest die Datei synchron ein
Schritt 2: API-Route erstellen (erste Version)¶
Wir erstellen eine Route /api, die die JSON-Daten asynchron ausliest:
const server = http.createServer((req, res) => {
const pathName = req.url;
if (pathName === '/' || pathName === '/overview') {
res.end('This is the OVERVIEW');
} else if (pathName === '/product') {
res.end('This is the PRODUCT');
} else if (pathName === '/api') {
fs.readFile(`${__dirname}/dev-data/data.json`, 'utf-8', (err, data) => {
const productData = JSON.parse(data);
console.log(productData);
});
res.end('API');
}
});
server.listen(8000, '127.0.0.1', () => {
console.log('Listening to requests on port 8000');
});
Schritt 3: Daten zurücksenden¶
Der nächste Schritt ist, die Daten tatsächlich zurückzusenden. Wir müssen dem Browser sagen, dass wir JSON zurücksenden. Der Status Code 200 sagt, dass alles in Ordnung ist.
} else if (pathName === '/api') {
fs.readFile(`${__dirname}/dev-data/data.json`, 'utf-8', (err, data) => {
const productData = JSON.parse(data);
res.writeHead(200, { 'Content-type': 'application/json' });
res.end(data);
});
}
HTTP Status Codes
- 200: Erfolgreiche Anfrage
- 404: Seite nicht gefunden
- 500: Serverfehler
Schritt 4: Top-level Code (Optimierung)¶
Performance-Tipp
Es macht Sinn, wenn wir die Daten ganz am Anfang einmal einlesen. Das wird nur einmal passieren, deshalb kann es ohne Probleme synchron sein. Wird etwas immer wieder wiederholt, blockiert es und wir sollten asynchron verwenden.
const fs = require('fs');
const http = require('http');
// Top-level code: Wird nur einmal beim Start ausgeführt
const data = fs.readFileSync(`${__dirname}/dev-data/data.json`, 'utf-8');
const dataObj = JSON.parse(data);
const server = http.createServer((req, res) => {
const pathName = req.url;
if (pathName === '/' || pathName === '/overview') {
res.end('This is the OVERVIEW');
} else if (pathName === '/product') {
res.end('This is the PRODUCT');
} else if (pathName === '/api') {
res.writeHead(200, { 'Content-type': 'application/json' });
res.end(data);
} else {
res.writeHead(404, {
'Content-type': 'text/html',
'my-own-header': 'hello-world',
});
res.end('<h1>Page not found!</h1>');
}
});
server.listen(8000, '127.0.0.1', () => {
console.log('Listening to requests on port 8000');
});
Server starten¶
Ausgabe:
Ergebnis im Browser¶
Wenn Sie nun im Browser 127.0.0.1:8000/api eintippen, sehen Sie die Daten der API:
[
{
"id": 0,
"productName": "Fresh Avocados",
"image": "🥑",
"from": "Spain",
"nutrients": "Vitamin B, Vitamin K",
"quantity": "4 🥑",
"price": "6.50",
"organic": true,
"description": "A ripe avocado yields to gentle pressure when held in the palm of the hand and squeezed..."
},
{
"id": 1,
"productName": "Goat and Sheep Cheese",
"image": "🧀",
"from": "Portugal",
"nutrients": "Vitamin A, Calcium",
"quantity": "250g",
"price": "5.00",
"organic": false,
"description": "Creamy and distinct in flavor, goat cheese is a dairy product enjoyed around the world..."
}
]
Erste API erstellt
Wenn wir diese Daten nun auf einen öffentlich zugänglichen Server legen würden, hätten wir unsere erste API gebaut, welche von anderen genutzt werden kann.
Beispiel einer öffentlichen API: https://api.thecatapi.com/v1/images/search?limit=10
HTML Templates¶
2.10 Aufgabe: Erstellung API mit HTML Template¶
Ziel
Eine kleine API mit HTML Template erstellen
Aufgabe: Erstellen Sie eine API für folgendes Szenario:
Sie würden gerne eine Car API erstellen. Die API soll 3 verschiedene Autos mit 4 Eigenschaften (Felder) Ihrer Wahl in einer data.json auflisten (z.B. Id, Bezeichnung, etc.). Erstellen Sie die nötige Route dafür, um das data.json zu laden und erstellen Sie auch ein dazugehöriges .html Template dafür.
Ziel der Car API Aufgabe¶
Ziel: Daten (JSON) von der Darstellung (HTML) trennen. Es sollten drei Autos mit je vier Eigenschaften in einer Datei gespeichert und über eine Webseite ansprechend angezeigt werden.
HTML-Template Erklärung¶
Template-Konzept
Das Template in templates/car-template.html ist wie ein Lückentext.
- Anstatt feste Namen einzutragen, nutzen wir Platzhalter wie
{%MARKE%}oder{%FARBE%} - Der Node.js-Server ersetzt diese Platzhalter bei jedem Aufruf mit echten Daten aus der
data.json
Vorteil: Wir müssen das Design nur einmal bauen. Der Server füllt die Lücken automatisch mit den aktuellen Daten.
Ablauf der Umsetzung¶
- Daten-Setup: Erstellung der
data.jsonmit drei Auto-Objekten (ID, Marke, Modell, Farbe, Baujahr) - Design-Setup: Erstellung des HTML-Templates im Unterordner
/templatesmit Platzhaltern für die Daten - Server-Logik:
- Dateien beim Start einmalig einlesen (
fs.readFileSync) - Routing erstellen:
/für die Webseite,/apifür die Rohdaten - Daten-Mapping: Der Server nimmt das Template und ersetzt die Lücken (
.replace()) durch die Werte aus dem ersten Auto-Objekt (dataObj[0]) - Lokaler Start: Server auf Port 8000 unter
127.0.0.1starten und im Browser testen
Projektstruktur¶
Code-Implementierung¶
index.js¶
const http = require('http');
const fs = require('fs');
// 1. Daten und Template laden (Pfad angepasst auf den Unterordner 'templates')
const tempCar = fs.readFileSync(`${__dirname}/templates/car-template.html`, 'utf-8');
const data = fs.readFileSync(`${__dirname}/data.json`, 'utf-8');
const dataObj = JSON.parse(data); // JSON in JavaScript-Objekt umwandeln
const server = http.createServer((req, res) => {
const pathName = req.url;
// Route für das HTML-Design
if (pathName === '/' || pathName === '/car') {
res.writeHead(200, { 'Content-type': 'text/html; charset=utf-8' });
// Platzhalter im Template mit Daten des ersten Autos ersetzen
let output = tempCar.replace(/{%MARKE%}/g, dataObj[0].marke);
output = output.replace(/{%MODELL%}/g, dataObj[0].modell);
output = output.replace(/{%FARBE%}/g, dataObj[0].farbe);
output = output.replace(/{%BAUJAHR%}/g, dataObj[0].baujahr);
res.end(output);
// API Route: Sendet die rohen JSON-Daten
} else if (pathName === '/api') {
res.writeHead(200, { 'Content-type': 'application/json; charset=utf-8' });
res.end(data); // Gibt den Inhalt der data.json zurück
} else {
// 404 Fehlerseite
res.writeHead(404, { 'Content-type': 'text/html; charset=utf-8' });
res.end('<h1>Seite nicht gefunden!</h1>');
}
});
// Lokal auf Port 8000 hören
server.listen(8000, '127.0.0.1', () => {
console.log('Server läuft lokal auf http://127.0.0.1:8000');
});
Code-Erklärung - Template-Logik
- Zeile 5-7: Wir lesen Template und Daten beim Start einmal ein (Top-level Code)
- Zeile 17-20: Mit
.replace()ersetzen wir die Platzhalter durch echte Daten - Zeile 17: Das
/gFlag bedeutet "global" - alle Vorkommen werden ersetzt - Zeile 17:
dataObj[0]greift auf das erste Auto aus dem Array zu - Zeile 18-20: Jede Zeile ersetzt einen weiteren Platzhalter im Template
- Zeile 26: Bei
/apisenden wir die rohen JSON-Daten zurück
Wichtig: Die Ersetzung erfolgt nacheinander, wobei jede replace()-Operation auf dem Ergebnis der vorherigen arbeitet.
templates/car-template.html¶
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Mein Auto-Katalog</title>
<style>
body {
font-family: sans-serif;
background-color: #f5f5f5;
padding: 20px;
}
h1 {
color: #1a237e;
text-align: center;
}
.car-card {
background: white;
padding: 20px;
margin: 20px auto;
max-width: 600px;
border-radius: 8px;
box-shadow: 2px 2px 10px rgba(0,0,0,0.1);
}
.car-card h2 {
color: #283593;
margin-top: 0;
}
.car-card p {
line-height: 1.6;
}
a {
display: block;
text-align: center;
color: #1a237e;
text-decoration: none;
margin-top: 20px;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<h1>Auto-Steckbrief</h1>
<div class="car-card">
<h2>{%MARKE%} {%MODELL%}</h2>
<p><strong>Marke:</strong> {%MARKE%}</p>
<p><strong>Modell:</strong> {%MODELL%}</p>
<p><strong>Farbe:</strong> {%FARBE%}</p>
<p><strong>Baujahr:</strong> {%BAUJAHR%}</p>
</div>
<a href="/api">Zur JSON-Rohansicht</a>
</body>
</html>
Template-Platzhalter
Die Platzhalter {%MARKE%}, {%MODELL%}, {%FARBE%} und {%BAUJAHR%} werden vom Server durch die echten Werte ersetzt. Die geschwungenen Klammern und Prozentzeichen sind eine gängige Konvention für Template-Platzhalter.
data.json¶
[
{
"id": 0,
"marke": "Porsche",
"modell": "911 Carrera",
"farbe": "Schwarz",
"baujahr": "2023"
},
{
"id": 1,
"marke": "BMW",
"modell": "M3 Competition",
"farbe": "Blau-Metallic",
"baujahr": "2024"
},
{
"id": 2,
"marke": "Mercedes-Benz",
"modell": "AMG GT",
"farbe": "Silber",
"baujahr": "2023"
}
]
Server-Befehle¶
| Befehl | Aktion |
|---|---|
node index.js |
Startet den Server auf deinem PC |
URL: localhost:8000/ |
Zeigt den Auto-Steckbrief (HTML) |
URL: localhost:8000/api |
Zeigt die Liste aller Autos (JSON) |
Ausgabe im Terminal¶
Ergebnis im Browser¶
Bei localhost:8000/ - Nach der Platzhalter-Ersetzung:
Die Platzhalter im HTML-Template werden durch folgende Werte ersetzt:
- {%MARKE%} → Porsche
- {%MODELL%} → 911 Carrera
- {%FARBE%} → Schwarz
- {%BAUJAHR%} → 2023
Darstellung im Browser:
Auto-Steckbrief
Porsche 911 Carrera
Marke: Porsche
Modell: 911 Carrera
Farbe: Schwarz
Baujahr: 2023
[Link: Zur JSON-Rohansicht]
Bei localhost:8000/api:
[
{
"id": 0,
"marke": "Porsche",
"modell": "911 Carrera",
"farbe": "Schwarz",
"baujahr": "2023"
},
{
"id": 1,
"marke": "BMW",
"modell": "M3 Competition",
"farbe": "Blau-Metallic",
"baujahr": "2024"
},
{
"id": 2,
"marke": "Mercedes-Benz",
"modell": "AMG GT",
"farbe": "Silber",
"baujahr": "2023"
}
]
Template erfolgreich erstellt
Die Webseite zeigt nun einen schön formatierten Auto-Steckbrief mit den Daten aus der JSON-Datei an. Die .replace()-Methode hat alle Platzhalter korrekt durch die entsprechenden Werte aus dataObj[0] ersetzt. Über den Link kann man zur Rohansicht der JSON-Daten wechseln.
API auf Raspberry PI laden¶
2.5 API auf Raspberry PI laden - 5P¶
Ziel
Sie können eine API auf einem Webserver erstellen und darauf zugreifen
Aufgabenbeschreibung: Erstellen Sie eine API mit personalisierten Fake-Daten und machen Sie die API auf dem persönlichen Ordner auf dem Raspberry PI innerhalb des Dockers abrufbar.
Abgabe: Bitte dokumentieren und per Link abgeben
Übersicht¶
Diese Aufgabe baut eine vollständige API auf dem Raspberry PI auf. Die API liefert Daten über Videospiele und demonstriert professionelles Routing, Zufalls-Logik und JSON-Verarbeitung.
Endpunkt-Übersicht¶
| Pfad | Antwort | Format |
|---|---|---|
/ oder /ueberblick |
Begrüßung & Link zur API | HTML |
/api oder /api/games |
Daten eines zufälligen Spiels | JSON |
| Sonstiges | Fehlerseite (404) | Text |
Projektstruktur¶
Code-Implementierung¶
server.js¶
const http = require('http');
// Dein spezifischer Port für den Raspberry Pi
const port = 4008;
const server = http.createServer((req, res) => {
const headers = {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*' // Wichtig, falls du die Daten später in einer Website anzeigen willst
};
if (req.url === '/' || req.url === '/ueberblick') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>Gaming Library - Pi Server</h1><p>Gehe zu <a href="/api">/api</a> für die Daten.</p>');
}
else if (req.url === '/api' || req.url === '/api/games') {
res.writeHead(200, headers);
const gameLibrary = [
{ id: 1, titel: "The Legend of Zelda: Breath of the Wild", genre: "Action-Adventure", release: 2017, plattform: "Nintendo Switch" },
{ id: 2, titel: "Red Dead Redemption 2", genre: "Action-Adventure", release: 2018, plattform: "PlayStation 4" },
{ id: 3, titel: "Elden Ring", genre: "Action-RPG", release: 2022, plattform: "PC" },
{ id: 4, titel: "God of War", genre: "Action-Adventure", release: 2018, plattform: "PlayStation 4" },
{ id: 5, titel: "Hades", genre: "Roguelike", release: 2020, plattform: "PC" },
{ id: 6, titel: "Minecraft", genre: "Sandbox", release: 2011, plattform: "Multi-Platform" }
];
const randomIndex = Math.floor(Math.random() * gameLibrary.length);
const randomGame = gameLibrary[randomIndex];
res.end(JSON.stringify(randomGame, null, 2));
}
else {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Seite nicht gefunden');
}
});
// Wir nutzen '0.0.0.0', damit der Pi im Netzwerk unter seiner IP 10.27.160.12 erreichbar ist
server.listen(port, '0.0.0.0', () => {
console.log(`Server läuft! Adresse: http://10.27.160.12:${port}/api`);
});
Was macht der Code?
Die wichtigsten Bausteine:
- Routing: Der Server prüft
req.url, um zu entscheiden, welche "Seite" geladen wird - Zufalls-Logik: Mit
Math.random()wird bei jedem Laden der API-Seite ein anderes Spiel aus der Liste ausgewählt - JSON-Ausgabe: Das Objekt wird mit
JSON.stringify()in ein Format umgewandelt, das Computer (und Menschen) leicht lesen können
Chronologie der Erstellung (Schritt für Schritt)¶
- Code-Erstellung: Schreiben der
server.jsmit den personalisierten Gaming-Daten - Port-Festlegung: Der Port wurde auf 4008 gesetzt
- Docker-Deployment: Hochladen der Datei auf den Pi und Starten eines Docker-Containers, um die App isoliert laufen zu lassen
- Netzwerk-Freigabe: Verwendung von
0.0.0.0im Code und-p 4008:4008im Docker-Befehl, damit der Zugriff über die IP10.27.160.12klappt
Problemanalyse & Lösung¶
Troubleshooting-Story
Während der Entwicklung bin ich auf mehrere Probleme gestoßen. Hier ist, wie ich sie gelöst habe:
| Problem | Ursache | Lösung & Command |
|---|---|---|
| Seite nicht erreichbar | Der Container war abgestürzt oder gar nicht gestartet (fehlte bei docker ps) |
Container neu starten mit Port-Mapping:docker run -d --name lars-app -p 4008:4008 -v $(pwd):/app -w /app node:24-alpine node server.js |
| MODULE_NOT_FOUND | Node.js hat im Container nach der Standarddatei index.js gesucht |
Den korrekten Dateinamen im Startbefehl angeben:node server.js (statt index.js) |
| Pfad-Fehler | Docker konnte das Verzeichnis nicht spiegeln, weil der Befehl im falschen Ordner ausgeführt wurde | In das richtige Verzeichnis wechseln und Dateien prüfen:cd ~/class/lars und danach ls |
| Container-Konflikt | Ein alter Container mit dem gleichen Namen blockierte den Neustart | Den alten Container gewaltsam entfernen:docker rm -f lars-app |
Chronologie: Vom Problem zur Lösung¶
- Start-Versuch: Die API war über die IP nicht erreichbar
- Fehlersuche:
docker pszeigte, dass mein Container nicht lief.docker logsverriet: Node.js suchteindex.js, aber meine Datei hießserver.js - Lösung: Löschen des alten Containers und Neustart mit dem korrekten Dateinamen und Port-Mapping
Die genutzten Befehle¶
Wichtige Docker-Befehle
| Befehl | Zweck |
|---|---|
ls |
Dateinamen prüfen (Ergebnis: server.js) |
docker ps |
Prüfen, ob der Container läuft |
docker rm -f lars-app |
Fehlerhaften Container löschen |
docker run -d --name lars-app -p 4008:4008 -v $(pwd):/app -w /app node:24-alpine node server.js |
Finaler Startbefehl: Verknüpft Port 4008 und startet server.js |
Ergebnis¶
API erfolgreich deployed
Die API ist nun unter http://10.27.160.12:4008/api stabil erreichbar und liefert bei jedem Refresh ein zufälliges Videospiel aus.
Beispiel-Ausgabe bei Aufruf von /api:
Beispiel-Ausgabe bei einem weiteren Aufruf:
Zusammenfassung¶
In diesem Block haben wir gelernt:
- ✅ Wie man einen einfachen Webserver mit Node.js erstellt
- ✅ Wie Routing funktioniert und verschiedene Pfade behandelt werden (Aufgabe 2.3)
- ✅ Wie man JSON-Daten einliest und über eine API bereitstellt (Aufgabe 2.4)
- ✅ Wie man HTML-Templates mit Platzhaltern verwendet und diese ersetzt (Aufgabe 2.10)
- ✅ Wie man eine API auf einem Raspberry PI mit Docker deployed (Aufgabe 2.5)
Wichtige Erkenntnisse
- Top-level Code wird nur einmal beim Start ausgeführt und ist ideal für Daten, die nur einmal geladen werden müssen
- Asynchroner Code wird verwendet, wenn Operationen wiederholt werden oder lange dauern könnten
- Templates ermöglichen eine saubere Trennung von Daten und Darstellung
- Docker isoliert Anwendungen und macht sie portabel