Zum Inhalt

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: ueberblick
  • produkte → 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 /ueberblick wird ein H1-Element mit "ueberblick" zurückgegeben
  • Zeile 10-12: Bei /produkte wird 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:

cd /home/m295/class/lars
ls
./start.sh

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:

<h1>ueberblick</h1>

Darstellung im Browser:

ueberblick
(als große Überschrift)

Bei Aufruf von http://10.27.160.12:4008/produkte:

<h2>hier sehen Sie unsere Produkte</h2>

Darstellung im Browser:

hier sehen Sie unsere Produkte
(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

  • __dirname bezeichnet den Pfad, an welchem gerade unsere index.js liegt
  • JSON.parse() wandelt JSON-Daten in JavaScript Code um
  • readFileSync() 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

node index.js

Ausgabe:

Listening to requests on port 8000

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

  1. Daten-Setup: Erstellung der data.json mit drei Auto-Objekten (ID, Marke, Modell, Farbe, Baujahr)
  2. Design-Setup: Erstellung des HTML-Templates im Unterordner /templates mit Platzhaltern für die Daten
  3. Server-Logik:
  4. Dateien beim Start einmalig einlesen (fs.readFileSync)
  5. Routing erstellen: / für die Webseite, /api für die Rohdaten
  6. Daten-Mapping: Der Server nimmt das Template und ersetzt die Lücken (.replace()) durch die Werte aus dem ersten Auto-Objekt (dataObj[0])
  7. Lokaler Start: Server auf Port 8000 unter 127.0.0.1 starten und im Browser testen

Projektstruktur

project/
├── index.js
├── data.json
└── templates/
    └── car-template.html

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 /g Flag 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 /api senden 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

$ node index.js
Server läuft lokal auf http://127.0.0.1:8000

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

/home/m295/class/lars/
├── server.js
├── start.sh
└── stop.sh

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)

  1. Code-Erstellung: Schreiben der server.js mit den personalisierten Gaming-Daten
  2. Port-Festlegung: Der Port wurde auf 4008 gesetzt
  3. Docker-Deployment: Hochladen der Datei auf den Pi und Starten eines Docker-Containers, um die App isoliert laufen zu lassen
  4. Netzwerk-Freigabe: Verwendung von 0.0.0.0 im Code und -p 4008:4008 im Docker-Befehl, damit der Zugriff über die IP 10.27.160.12 klappt

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

  1. Start-Versuch: Die API war über die IP nicht erreichbar
  2. Fehlersuche: docker ps zeigte, dass mein Container nicht lief. docker logs verriet: Node.js suchte index.js, aber meine Datei hieß server.js
  3. 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:

{
  "id": 3,
  "titel": "Elden Ring",
  "genre": "Action-RPG",
  "release": 2022,
  "plattform": "PC"
}

Beispiel-Ausgabe bei einem weiteren Aufruf:

{
  "id": 5,
  "titel": "Hades",
  "genre": "Roguelike",
  "release": 2020,
  "plattform": "PC"
}

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