F73 - Vertiefung: Webentwicklung - Term 7 - Thema: Node.js

Dieser Leitfaden dient einerseits der Vorbereitung und Durchführung der Trainings und zusätzlich kann der Leitfaden durch inhaltliche Ergänzungen so erweitert werden, dass er im Anschluss an die Teilnehmer verteilt werden kann.

Ziel des hier beschriebenen Kurses ist es, einen ersten Einblick in die Arbeit mit dem Framework Node.js zu bekommen, indem eine vollständige 3-Schichten-Applikation um gesetzt wird (Datenbank, Backend, Frontend).

WICHTIG: Dieses Dokument listet nur die allgemeinen Inhalte mit etwas Erklärung auf. Es dient daher nur der Begleitung des Kurses. Es ist nicht darauf ausgelegt, ALLE relevanten Inhalte vollständig zu erläutern.

1. Präambel

1.1. Aufbau des Kurses

Der Kurs wird in insgesamt 5 Teile aufgeteilt:

  1. Wiederholung von Frontend-Technologien - 2 Tage: An diesen beiden Tagen wird jeweils eine kleine Minianwendung mit den Technologien React & Angular umgesetzt. Hierbei wird noch kein Backend angesprochen, um die Inhalte simpel zu halten.

  2. Node.js & SQLite - 2 Tage: Wiederholung des Toolings rund um Node.js (node, npm, package.json etc.) und erste Arbeit mit der Datenbank

  3. HTTP & Express - 2 Tage: Wiederholung der HTTP-Grundlagen und Umsetzung erster Endpunkte mit Express

  4. Anbindung an Frontend und weitere Features - 2 Tage: Die umgesetztn Endpunkte sollen jetzt an eines der entwickelten Frontends angeschlossen werden. Zusätzliche Funktionen, wie Sessions oder Login können zusätzlich umgesetzt werden.

  5. Projektarbeit: Das gelernte wird an einem neuen Use Case umgesetzt

1.2. Anwendungsfälle

Im Theorieteil des Kurses werden wir uns mit einer neuen Applikation beschäftigen, und als Zusatzinhalt wieder auf unsere bewährte Todo-Applikation zurückgreifen.

Screenshot der Zitate-App

  1. Zitate-App: In dieser Applikation können Nutzer sich ein zufälliges Zitat ansehen. Zusätzlich sollen noch andere Zitate vom gleichen Autor dargestellt werden können. Als Bearbeitungsfunktion, können neue Autoren und auch Zitate erstellt werden. Die Umsetzung dieser Applikation wird im geführten Teil des Kurses in gemeinsamer Arbeit erfolgen
  2. Todo-App: Selbständige Projektarbeit zum festigen und ausprobieren.

Wieso benutzen wir Frameworks?

2. Wiederholung von Frontend-Technologien

In diesem Teil sollen die absoluten Basics von Angular & React beschrieben. Das sind beides Frameworks, mit denen Single Page Applications - SPA umgesetzt werden können. Also Applikationen, die im Browser laufen und sich eher wie Desktopanwendungen oder Apps verhalten, als wie Webseiten. Diese basieren darauf, dass JavaScript im Browser läuft, um auf Nutzereingaben zu reagieren.

Beide Frameworks sind Komponentenbasiert, das heißt, wir zerschneiden unsere Oberfläche in kleinere Blöcke, die wir wiederverwenden können.

2.1. Angular

2.1.1. Setup

🎓 Wissen

Angular bringt eine eigene CLI (Command Line Interface) mit, um Angular-Projekte aufzusetzen. Damit sind unter anderem die Tools TypeScript, Webpack & SCSS vorkonfiguriert, sodass wir diese nutzen können, ohne weitere Konfiguration vornehmen zu müssen.

Diese können wir über das npm-Tool npx benutzen:

npx --package @angular/cli@13.1.2 -- ng new angular-app

npx installiert dabei das Paket @angular/cli in der Version 13.1.2 (aktuellste Version mit Stand 27.12.2021) und führt mit diesem Paket das ng-Kommando aus und generiert damit ein neues Projekt mit dem Namen "angular-app". Die CLI führt uns im Weiteren durch eine einfach Konfiguration (Achtung, die gestellten Fragen hängen von der Version der Angular-CLI ab.)

Achtung: In diesem Kurs arbeiten wir mit modernem JavaScript/TypeScript. Wir arbeiten also nicht mit globalen Script-Dateien sondern nur mit Modulen aus denen wir bestimmte Werte exportieren, um sie in anderen Modulen zu importieren. Zur Wiederholung von Modulen kann die TypeScript-Dokumentation gelesen werden: https://www.typescriptlang.org/docs/handbook/2/modules.html

Aufgabe

🎯 Ziel: Ein neues Angular Projekt ist angelegt, kann gestartet werden und erste Interfaces sind definiert.

2.1.2. Grundlagen einer Komponente

In Angular sind alle UI-Komponenten als Klassen definiert.

Klasse

Ähnlich wie in Java oder anderen Objekt-Orientierten Programmiersprachen können in TypeScript Klassen definiert werden. (siehe TypeScript-Dokumentation)

Klassen haben dabei Felder und Methoden.

Die Klasse beschreibt den Logik-Part einer Komponente. Also zum Beispiel, welche Daten in der UI dargestellt werden, was passiert, wenn ein Nutzer mit der Oberfläche interagiert, etc. Neben der Klasse gibt es das Template, und die Style-Dateien.

Template Im Template können wir einerseits normales HTML schreiben, um die Oberfläche der Komponente zu beschreiben. Zusätzlich können direkt Berechnungen im Template einbetten, indem wir die geschweiften Klammern nutzen: {{ 1 + 1 }}. In diesen Ausdrücken können wir auf alle Felder der dazugehörigen Klasse zugreifen. Achtung: Die Ausdrücke im Template sollten so simpel wie möglich sein, damit der Code wartbar und skalierbar bleibt.

Modul

Zusätzlich zu den TypeScript-Modulen bringt Angular seine eigenen Module mit, damit wir unsere Applikationen besser auftrennen können. In zukünftigen Angular-Versionen werden diese Module vermutlich nicht mehr nötig sein. Aktuell müssen aber alle Komponenten, die wir benutzen wollen, in einem Module aufgehängt sein.

Aufgabe

🎯 Ziel: Erste richtige Informationen werden in der Oberfläche dargestellt.

2.1.3. Data Layer

🎓 Wissen

Services Um reine Datenverarbeitung und Logik abzubilden, benutzen wir in Angular Services: Klassen, die Logik enthalten und die in Komponenten eingehängt werden können. Diese werden in Angular automatisch per Dependency Injection mit unseren Komponenten verknüpft. Das heißt, unsere Komponenten importieren nicht direkt die Funktionen, die benutzt werden sollen, sondern nur die Typdefinition. Angular kümmert sich dann automatisch darum, dass eine Instanz der Klasse erzeugt wird und an unsere Komponente übergeben wird.

Sie können mit Hilfe der Angular-CLI automatisch erzeugt werden:

npx ng generate service quotes

Services müssen nicht im Angular-Module registriert werden.

Mehr dazu in der Angular-Dokumentation.

Observables

In Angular werden asynchrone Daten (Daten, die erst nach einer bestimmten Zeit verfügbar sind, wie zum Beispiel Resultate von Netzwerk-Abfragen) mit Hilfe von Observables umgesetzt. Diese können im Laufe der Zeit 0, einen oder mehrere Werte produzieren. Sie orientieren sich am Observer-Pattern.

import { Observable, of } from "rxjs"; import { Quote } from "src/app/api.types"; function loadDummyData(): Observable<Quote> { const dummyQuote: Quote = { id: 1, text: "Quote", author: { id: 1, name: "Author", }, }; // Wir benutzen die of-Funktion von Rx.js um // ein Observable mit einem definierten Wert zu // erzeugen. // So können wir später den richtigen Netzwerk-Request // einbauen, ohne unsere Komponente anzufassen. return of(dummyQuote); } loadDummyData().subscribe((result) => { console.log(result); displayResult(result); });

Rx.js bietet zusätzlich eine Reihe von Operatoren, mit denen Observables transformiert werden können.

import { delay, Observable, of } from "rxjs"; import { Quote } from "src/app/api.types"; function loadDummyData(): Observable<Quote> { const dummyQuote: Quote = { id: 1, text: "Quote", author: { id: 1, name: "Author", }, }; // Wir nutzen den delay-Operator // um alle Werte in unserem Observable um 1000 Millisekunden // zu verzögern. return of(dummyQuote).pipe(delay(1000)); } loadDummyData().subscribe((result) => { console.log(result); displayResult(result); });

Mehr dazu in der Angular-Dokumentation

Aufgabe

🎯 Ziel: Dummy Daten können geladen werden, um sie erstmal auf die Konsole zu schreiben.

2.1.4. Template-Logik Teil 1

🎓 Wissen

Häufig haben wir die Situation, dass wir Teile der Oberfläche nur unter bestimmten Bedingungen anzeigen wollen: Ein Menü soll nur sichtbar sein, wenn der Nutzer auf einen Button geklickt hat, ein Button soll nur sichtbar sein, wenn der Nutzer eingeloggt ist, oder eine Liste soll nur sichtbar sein, wenn Daten fertig geladen sind. Dafür können wir in Angular ngIf benutzen:

<button *ngIf="4 > 3">Klick mich, wenn 4 größer als 3 ist</button> <button *ngIf="isAuthenticated()"> Klick mich, wenn du authentifiziert bist. </button>

Aufgabe

🎯 Ziel: Die Dummydaten werden richtig in der Oberfläche dargestellt und wir haben vorarbeit geleistet für reale Netzwerkkommunikation, indem wir einen Ladezustand mit abbilden.

2.1.5. Events

🎓 Wissen Bisher ist unsere Anwendung noch sehr langweilig, weil der Nutzer nicht damit interagieren kann. Wir möchten jetzt auf Events reagieren, das geht in Angular auch schön einfach mit Hilfe von Outputs:

@Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.scss"], }) export class AppComponent { count = 1; handleClick() { console.log("click"); count++; } }
<!-- Achtung! Funktion wird erst bei Klick aufgerufen. --> <button (click)="handleClick()">Klick mich {{ count }}</button>

Angular-Dokumentation

Aufgabe

🎯 Ziel: Neue Zitate können geladen werden.

2.1.6. Inputs

🎓 Wissen Bisher haben wir nur eine Komponente. Das wird einerseits auf Dauer unübersichtlich und der große Vorteil von Komponentenbasierten Frameworks ist ja, dass wir Komponenten wiederverwenden können. Dafür müssen wir erstmal Komponenten umsetzen. Diese Komponenten können dabei über sog. Inputs konfiguriert werden.

@Component({ selector: "child-component", template: ` <p>Hello {{userName}}</h3> `, }) export class HeroChildComponent { @Input() userName!: string; }
<child-component [userName]="'Peter ' + 'Pan'"></child-component>

Angular-Dokumentation

Wenn wir auf Änderungen von Inputs reagieren wollen, funktioniert das am einfachsten über JavaScript setter:

@Component({ selector: "child-component", template: ` <p>Hello {{userName}}</h3> `, }) export class HeroChildComponent { _userName!: string; @Input() set userName(newValue: string) { console.log(`New Value: ${newValue}`); this._userName = newValue; } }

Aufgabe

🎯 Ziel: Unsere erste Komponente mit Interface ist umgesetzt

2.1.7. Template-Logik Teil 2

🎓 Wissen

Wir wollen jetzt alle Zitate des Authors darstellen. Dafür müssen wir über eine Liste mit Zitat-Objekten iterieren, und für jedes ein Stück HTMl bauen. Das geht in Angular mit *ngFor:

<ul> <!-- Anstatt hier eine neue Liste zu erzeugen, können wir natürlich auch auf ein Feld auf der Komponente zugreifen. --> <li *ngFor="let i of [1, 2, 3]">{{ i }}</li> </ul>

Aufgabe

🎯 Ziel: Eine Liste der Zitate des Autors wird geladen und ist sichtbar.

2.1.8. Formulare

🎓 Wissen

Ein großer Teil von Webanwendungen sind immer die Formulare in der Applikation. Gerade weil der Anwendungsfall so wichtig ist, hat Angular dafür eine eingebaute Bibliothek, die den Zustand von Formularen verwalten kann.

Um die Formularbibliothek zu nutzen, müssen wir sie erstmal im Angular-Modul registrieren:

import { ReactiveFormsModule } from "@angular/forms"; @NgModule({ imports: [ // other imports ... ReactiveFormsModule, ], }) export class AppModule {}

Folgend können wir in Komponenten Formulare definieren:

@Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.scss"], }) export class AppComponent { form = new FormGroup({ username: new FormControl(""), password: new FormControl(""), }); handleSubmit() { console.log(this.form.value); } }

Die Form Controls müssen dann noch mit der UI verknüpft werden:

<!-- unsere FormGroup wird an den [formGroup] input gegeben --> <form [formGroup]="form" (ngSubmit)="handleSubmit()"> <!-- Das for Attribut verknüpft uns Label und input --> <label for="username">First Name: </label> <!-- Alle Controls brauchen dann den Namen des Feldes: --> <input id="username" type="text" formControlName="username" /> <br /> <label for="password">Last Name: </label> <input id="password" type="password" formControlName="password" /> <!-- Submit Button triggert automatisch das ngSubmit event --> <button type="submit">Speichern</button> </form>

Mehr dazu in der Angular-Dokumentation

Aufgabe

🎯 Ziel: Ein Formular ist sichtbar und kann abgeschickt werden.

2.1.9. Optionale Zusatzaufgaben

2.2. React

React ist, genau so wie Angular, ein Komponentenbasiertes UI-Framework. Allerdings verfolgt React die Philiosophie, weit weniger Entscheidungen schon im voraus zu treffen. Diese Entscheidungen liegen dann also bei uns. Einerseits bietet das viel Flexibilität, da man sich seine Architektur so zusammenbauen kann, wie es für das Projekt geeignet ist, andererseits setzt das auch die Kompetenz voraus, diese Entscheidungen treffen zu können.

React-Dokumentation

2.2.1. Setup

Anders als bei Angular gibt es bei React keine offizielle CLI, die in "allen" Projekten genutzt wird. Wir werden für die folgenden Aufgaben den Vite-Bundler nutzen. Dieser ist sehr einfach zu konfigurieren, extrem performant und trotzdem flexibel genug für Enterprise-Applikationen.

Über Vite können trotzdem sehr schnell und einfach Projekte aufgesetzt werden:

npm init vite@latest

Jetzt bekommen wir eine Reihe von Fragen, die uns durch das Setup führen.

Aufgabe

🎯 Ziel: Ein neues React Projekt ist angelegt, kann gestartet werden und erste Interfaces sind definiert.

2.2.2. Grundlagen einer Komponente

🎓 Wissen Komponenten werden in React mit einfachen Funktionen definiert. Diese Funktionen haben einen etwas merkwürdigen Rückgabetyp:

function App() { return <button>Click me</button>; }

Es sieht aus, als würde unsere Funktion HTML zurückgeben. In Wirklichkeit, wird hier aber nur ein Objekt erzeugt, welches beschreibt, wie die Oberfläche auszusehen hat, React kümmert sich dann darum, dass das im Browser auch wirklich genau diese Elemente dargestellt werden. In einem TSX-File sind wir dadurch immer in 2 unterschiedlichen Kontexten: Auf TypeScript-Ebene oder auf TSX ebene, wo wir HTMl schreiben können. Um von der TypeScript-Welt in die TSX-Welt zu kommen, schreiben wir einfach einen TSX-Ausdruck:

function Component() { const value1 = 1; const value2 = 2; // value1 wird als Text interpretiert! return <button>TSX here! value1</button>; }

Und um wieder zurück in die TypeScript-Welt zu kommen, nutzen wir geschweifte Klammern:

function Component() { const value1 = 1; const value2 = 2; // value1 wird als Text interpretiert! return <button id={`id-${value1}`}>{valur1 + value2}</button>; }

Mehr Details gibt es in der React Dokumentation.

Aufgabe

🎯 Ziel: Erste richtige Informationen werden in der Oberfläche dargestellt.

2.2.3. Events und Zustand

🎓 Wissen

Veränderliche Daten müssen in React explizit mit Hilfe der useState-Hook deklariert werden. Diese kann nur im "Hauptbereich" einer Komponente benutzt werden, NICHT innerhalb von Event-Handlern oder Schleifen.

function Button() { // Use state gibt ein Tupel zurück: // Eine Liste mit zwei Elementen // 1. dem aktuellen Zustand // 2. einer Funktion zum Updaten des Zustands const [count, setCount] = useState(1); // Updates können NUR über setCount gespeichert werden. // Die count Variable selbst kann von uns nicht editiert werden return <button onClick={() => setCount(count + 1)}>{count}</button>; }

React Dokumentation zu Interaktivität in React-Apps

Aufgabe

🎯 Ziel: Das aktuelle Zitat kann per Knopfdruck ausgetauscht werden

2.2.4. Data Layer

🎓 Wissen

React braucht keine Service-Klassen sondern nutzt öfter statische Funktionen. Diese dürfen wir aber nicht so ohne weiteres in unseren Kompontenen aufrufen! Die Funktionen, mit denen wir Komponenten definieren, werden mit JEDER Änderung von Zuständen erneut aufgerufen. Wenn wir also beim Aufruf eine Netzwerabfrage starten würden, würde diese bei JEDER Zustandsänderung erneut abgeschickt werden. Solche Art von Nebenwirkungen nennt man in der Programmierung "Side Effects". Diese werden in React mit der useEffect-Hook abgebildet. Auch hier gilt wieder die Regel, dass diese Hook NIEMALS in einem Event-Handler oder in einer Schleife aufgerufen werden darf.

function App() { const [count, setCount] = useState(0); useEffect(() => { console.log(count); // Wir geben hier die Abhängigkeiten dieser Nebenwirkung an // Diese Funktion wird also immer dann aufgerufen, wenn sich count // ändert. }, [count]); useEffect(() => { console.log(count); // Liste der Dependencies ist leer -> Wird nur beim ersten Durchlauf ausgeführt. }, []); return <button onClick={() => setCount(count + 1)}>Click</button>; }

Mehr Informationen zu Funktionen und Nebenwirkungen gibt es hier

Netzwerkabfragen mit Promises React schreibt selber nicht vor, wie wir Netzwerkabfragen umsetzen sollen, Promises haben sich hier aber als Standardmodell durchgesetzt:

function loadData() { // Hier erzeugen wir ein neues Promise, was erst nach 1000ms "fertig" ist. const delayedPromise = new Promise((r) => setTimeout(r, 1000)); const delayedResult = delayedPromise.then(() => { // Wird nach 1000ms ausgeführt. return { x: 1 }; }); // Wir geben ein Promise zurück, // welches nach 1000ms das Objekt {x: 1} zurückgibt. return deplayedResult; } loadData().then((result) => { // Wird erst aufgerufen, wenn loadData fertig ist. console.log(result.x); });

Aufgabe

🎯 Ziel: Dummy Daten können mit Hilfe einer zentralen API geladen werden.

2.2.5. Template-Logik Teil 1

🎓 Wissen

Anstatt eine eingebaute Syntax zu haben, nutzt React für Control-Flow im Template einfach JavaScript Konstrukte:

function Counter() { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count + 1)}>Zähler: {count}</button> {count > 10 ? ( <p>Der Zähler ist schon ganz schön hoch...</p> ) : ( <p>Sehr niedriger Zähler</p> )} {count % 2 === 0 ? "Eine Gerade Zahl" : null} </div> ); }

Aufgabe

🎯 Ziel: Die Dummydaten werden richtig in der Oberfläche dargestellt und wir haben Vorarbeit geleistet für reale Netzwerkkommunikation, indem wir einen Ladezustand mit abbilden.

2.2.6. Props

🎓 Wissen

Was in der Angular Welt Inputs sind, sind in der React Welt "props". Die Arugmente der Komponten:

function Child(props: { name: string }) { return <p>Hallo {props.name}</p>; } function Parent() { const hardCodedName = "Test"; return <Child name={hardCodedName} />; }

Aufgabe

🎯 Ziel: Unsere erste Komponente mit Interface

2.2.7. Template-Logik Teil 2

🎓 Wissen

Wie beim bedingten Rendern auch, hat React für Schleifen kein eigenes Konstrukt, sondern baut auf JavaScript:

function List() { const list = [1, 2, 3]; return ( <ul> {list.map((item) => ( // React möchte beim Rendern von Listen immer den Key Prop // auf dem TSX-Element // Dadurch kann die Performance stark verbessert werden. // Der key muss für jeden Eintrag in DIESER Liste eindeutig sein. <li key={item}>{item}</li> ))} </ul> ); }

Aufgabe

🎯 Ziel: Eine Liste der Zitate des Autors wird geladen und ist sichtbar.

2.2.8. Formulare

🎓 Wissen

React selbst bringt keine Formular-Bibliothek mit. Es gibt einige verbreitete Community-Projekte, wie zum Beispiel Formik. Wir werden das Formular aber erstmal ohne Bibliothek bauen.

Die einfachste Möglichkeit dafür ist, einfach ein useState pro Feld zu definieren:

import { FormEvent, useState } from "react"; function LoginForm() { const [name, setName] = useState(""); const [password, setPassword] = useState(""); function handleSubmit(event: FormEvent<HTMLFormElement>) { // Achtung, in React müssen wir den Browserstandard selbst deaktivieren, // sonst lädt die Seite mit jedem Submit neu. event.preventDefault(); console.log({ name, password }); } return ( <form onSubmit={handleSubmit}> {/* Achtung: for gibt es in React nicht, wir müssen htmlFor verwenden */} <label htmlFor="username">Name</label> <input id="username" type="text" value={name} onChange={(e) => setName(e.target.value)} /> <label htmlFor="password">Passwort</label> <input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> <button type="submit">Speichern</button> </form> ); }

Aufgabe

🎯 Ziel: Ein Formular ist sichtbar und kann abgeschickt werden.

2.2.9. Optionale Zusatzaufgaben

3. Einstieg in die Backend-Welt

Wie kommt es das JavaScript auch im Backend verwendet werden kann, wenn es ursprünglich mal für den Browser entwickelt wurde? Das lässt sich durch die folgende leichte Folgekette erklären:

  1. JavaScript wird immer mehr im Web benutzt und ist sehr simpel. Man hat schnell Erfolgserlebnisse
  2. Dadurch gibt es im Laufe der Zeit immer mehr und mehr Entwickler:innen
  3. Durch die vielen Fans der Sprache, gibt es früher oder später jemanden, der ausprobiert was alles möglich ist, und entwickelt eine Laufzeitumgebung für JavaScript auf dem Server: Node.js

Wenn wir in Projekten JavaScript bzw. TypeScript über den ganzen Stack hinweg einsetzen, entsteht ein riesiger Vorteil: Wir können ein Feature auf allen Ebenen implementieren mit nur einer Programmiersprache!

Im Laufe der nächsten 6 Tage werden wir an zwei Applikationen arbeiten: Dem Backend für unsere Zitate-App und einem anderen Backend für eine Todo-Anwendung. Die Zitate-App werden wir wieder in Zusammenarbeit am Vormittag entwickeln und nachmittags habt ihr dann Zeit, das gelernte in der Todo-App zu üben.

3.1. Projekt Setup

Aufgabe

🎯 Ziel: Wir haben ein Projekt, in dem wir TypeScript-Code mit Node ausführen können.

{ "compilerOptions": { "target": "ESNext", "module": "commonjs", "moduleResolution": "node", "noEmit": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true } }

3.2. Datenbasis

🎓 Wissen

Um Informationen über längere Zeit hinweg abzuspeichern (zu persistieren) brauchen wir ein weiteres Tool in unserem Stack: eine Datenbank! Es gibt eine Vielzahl von Datenbankmanagementsystemen (DBMS), die unterschiedliche Methoden anbieten, wie auf Daten zugegriffen werden kann.

Auch wenn viele dieser Systeme SQL im Namen tragen, verwenden sie leicht unterschiedliche Dialekte, so gibt es viele Funktionen, die von einem DBMS verstanden werden und von anderen nicht.

Wir wählen für unser Projekt SQLite, da wir hiermit sehr schnell starten können.

Aufgabe

🎯 Ziel: Wir machen uns vertraut mit der Datenbasis

3.3. better-sqlite3

🎓 Wissen

Um mit der Datenbank zu kommunizieren, verwenden wir das npm Paket: better-sqlite3:

import Database from "better-sqlite3"; const db = Database("../quotes.db", { fileMustExist: true }); // .all() lädt alle passenden Zeilen const authors = db.prepare(`SELECT * FROM author`).all(); // .get() extrahiert direkt den ersten Eintrag const author = db .prepare(`SELECT id, name FROM author WHERE name = 'Yoda'`) .get(); // Run führt ein statement ohne Rückgabe aus (insert, delete) db.prepare(`INSERT INTO author (name) VALUES ('Peter')`).run(); // Variablen sollten IMMER über Platzhalter an die Abfrage // übergeben werden! // Sonst machen wir uns angreifbar für SQL-Injections const otherAuthor = db .prepare(`SELECT id, name FROM author WHERE name = ?`) .get("Yoda"); // Mit ORDER BY RANDOM() // können wir die Ergebnisse mischen // mit LIMIT 1 nehmen wir dann nur die erste // Zeile const randomQuote = db .prepare( ` SELECT * FROM quote ORDER BY RANDOM() LIMIT 1 ` ) .get();

Aufgabe

🎯 Ziel: Du verstehst, wie better-sqlite3 bedient wird und erinnerst dich, wie SQL für die Abfrage von Daten funktioniert.

Lösung für Aufgabe

Das Grundsetup ist immer das gleiche:

import Database from "better-sqlite3"; const db = Database("./quotes.db", { fileMustExist: true });

Abfrage 1

const q1 = db .prepare( ` SELECT count(*) 'Anzahl Zitate' FROM quote ` ) .get(); console.log(q1);

Abfrage 2

const q2 = db .prepare( ` SELECT count(*) 'Anzahl Autoren' FROM author ` ) .get(); console.log(q2);

Abfrage 3

const q3 = db .prepare( ` SELECT count(*) as 'Zitate von Sokrates' FROM quote as q LEFT JOIN author as a ON a.id = q.authorId WHERE a.name = 'Sokrates' ` ) .get(); console.log(q3);

Abfrage 4

const q4 = db .prepare( ` SELECT q.text FROM quote as q LEFT JOIN author as a ON a.id = q.authorId WHERE a.name = 'Sokrates' ` ) .all(); console.log(q4.map((q) => q.text));

Abfrage 5

const q5 = db .prepare( ` SELECT q.text, a.name FROM quote as q LEFT JOIN author as a ON a.id = q.authorId ORDER BY RANDOM() LIMIT 1 ` ) .get(); console.log(q5);

Abfrage 6

const q6 = db .prepare( ` SELECT a.name, count(q.id) as Zitate FROM quote as q LEFT JOIN author as a ON a.id = q.authorId GROUP BY a.id ORDER BY count(q.id) DESC LIMIT 1 ` ) .get(); console.log(q6);

Abfrage 7

const q7 = db .prepare( ` INSERT INTO author (name) VALUES ('Petra') ` ) .run();

Abfrage 8

const q8 = db .prepare( ` INSERT INTO quote (text, authorId) VALUES (?, ?) ` ) .run("Neues Zitat", q7.lastInsertRowid); console.log(q8);

Neu angelegte Daten wieder löschen:

const deleteQuoteResult = db .prepare( ` DELETE FROM quote WHERE id > 10 ` ) .run(); console.log(deleteQuoteResult); const deleteAuthorResult = db .prepare( ` DELETE FROM author WHERE id > 10 ` ) .run(); console.log(deleteAuthorResult);

3.4. Typisierte Datenbank-Funktionen

🎓 Wissen

Der Rückgabetyp von SQL-Abfragen ist immer any, weil TypeScript nicht weiß, wie unsere Datenbank strukturiert ist. Hier müssen wir manuell nachhelfen:

import Database from "better-sqlite3"; const db = Database("../quotes.db", { fileMustExist: true }); type Quote = { id: number; text: string; }; function loadAllQuotes(): Quote[] { const queryResult = db.prepare(`SELECT * FROM quote`).all(); // Erinnerung: array.map transformiert alle Elemente! const quotes = queryResult.map( // Wir geben manuell den Rückgabetyp an (result): Quote => ({ id: result.id, text: result.text, }) ); // Alternativ: const firstResult = queryResult[0]; const firstQuote: Quote = { id: firstResult.id, text: firstResult.text, }; return quotes; } const allQuotes = loadAllQuotes(); // Wenn wir den Typ richtig angegeben haben, // bekommen wir hier dann einen Fehler // weil wir das Autor Feld nicht spezifiziert haben. allQuotes[0].author;

Wenn wir Einträge anlegen, können wir auf die id des erzeugten Eintrags zugreifen:

export function getAuthorById(id: number): Author | null { const result = db.prepare(`SELECT * FROM author WHERE id = ?`).get(id); if (!result) return null; return { id: result.id, name: result.name, }; } export function addAuthor(name: string): Author { const result = db.prepare(`INSERT INTO author (name) VALUES(?)`).run(name); // lastInsertRowid ist als Bigint typisiert, das müssen wir in // Number umwandeln. const added = getAuthorById(Number(result.lastInsertRowid))!; return added; }

Aufgabe

🎯 Ziel: Wir können aus unserer App mit der Datenbank interagieren und haben trotzdem Typsicherheit und Bequemlichkeit.

3.5. Erstes HTTP-Backend implementieren

🎓 Wissen

Das Web basiert auf einem Text-basierten Frage-Antwort-Protokoll. Clients (wie zum Beispiel unser Web-Browser) schicken Anfragen an einen Server, der die Anfrage bearbeitet und Text zurück schickt. Dieser Text könnte HTML, CSS, ein Bild oder etwas anderes enthalten.

Jede Anfrage wird dabei an einen Host (z.B. developer.mozilla.org), auf eine bestimmte Route (z.B. /en-US/docs/Web/HTTP/Overview) und mit einer bestimmten Methode (GET für Abfragen, POST für Updates, etc.) geschickt. Zusätzlich enthält die Anfrage im Kopfbereich (Header) bestimmte Metainformationen, wie zum Beispiel die Liste der unterstützten Dateiformate oder Sprachen. Der Server verarbeitet diese Anfrage dann und antwortet mit einem Statuscode (2XX für OK, 4XX für Fehler auf der Client-Seite und 5XX für Fehler auf der Serverseite.) Neben dem Status-Code schickt auch der Server Header zurück und einen Body, also den Inhalt der Antwort.

Mehr Details auf MDN

In diesem Kapitel wollen wir unseren ersten Endpunkt mit dem express Framework implementieren.

// Wir importieren das installierte Paket import express from "express"; // Und erzeugen eine neue App-Instanz const app = express(); // Diese App soll GET-Anfragen auf / Hello World antworten. app.get("/", function (req, res) { // Express-handler werden immer mit min. 2 Argumenten aufgerufen: // Dem Request-Objekt // Dem Response-Objekt console.log(req); console.log(res); res.send("Hello World"); }); // Alle anderen Methoden und Routen antworten mit "Backend running" app.all("*", function (req, res) { res.send("Backend running"); }); // Unser Backend läuft auf dem Netzwerk-Port 3001 app.listen(3001);

Aufgabe

🎯 Ziel: Ein erster Dummy-Endpunkt ist implementiert und kann im Browser betrachtet werden

3.6. Richtige Express-Endpunkte definieren

🎓 Wissen

Express ist ein "Middleware" basiertes Web-Framework. Eine Middleware ist ein Konstrukt, welches bei jedem Request zwischen dem Eingang der Abfrage und dem Verarbeiten der Abfrage geschaltet ist. Die Middleware kann die eingehende Abfrage modifizieren. Middlewares können zusätzlich in Schichten aufgebaut werden: Jede Middleware kann entscheiden, ob die nächste Schicht die Anfrage überhaupt bekommen soll, oder nicht:

import express from "express"; const app = express(); // Middlewares werden mit .use definiert app.use((req, res, next) => { const time = Date.now(); console.log(`${req.method}-Request at ${req.path}`); // Middlewares können das Request-Objekt modifizieren. req.headers["timestamp"] = time.toString(); // Und die Anfrage an die nächste Middleware weiterleiten. next(); }); app.use((req, res, next) => { const secret = req.headers.secret; if (secret !== "Streng geheim") { // Wenn der Header nicht gesetzt ist, // Geben wir den Status Code für fehlende Authentizierung zurück // Und beenden den Request res.status(401); res.end(); } else { // Nur wenn der Header gesetzt ist, // Lassen wir den Request an weitere Middlewares weiter. next(); } }); app.all("*", (req, res) => { console.log(req.body); // Der Timestamp Header ist jetzt vorhanden! console.log(req.headers); console.log(req.path); res.send("Hello World"); }); app.listen(3001);

Um jetzt den Body von Anfragen verarbeiten zu können, brauchen wir eine Middleware, die den Body in bestimmten Formaten einliest: Body Parser:

import bodyParser from "body-parser"; import express from "express"; const app = express(); app.use(bodyParser.json()); app.all("*", (req, res) => { // Durch den json body-parser kann JSON content ausgelesen werden. console.log(req.body); res.send("Hello World"); });

Neben dem Request-Body können Informationen auch noch über Pfad-Parameter ans Backend übergeben werden:

app.get("/users/:userId", (req, res) => { const userId = req.params.userId; console.log(userId); res.send(null); });

Aufgabe

🎯 Ziel: Alle nätigen Endpunkte für unsere Zitate-App sind implementiert

3.7. Frontend ans Backend anhängen

🎓 Wissen

Standardmäßig erlaubt der Browser nicht, dass Daten an URLs geschickt werden, die nicht auf der gleichen Domain liegen wie die aktuell geöffnete Seite. Wenn du also auf www.google.de bist, verbietet der Browser dir, dass du Daten von www.facebook.de anfordest. Dieses Konzept wird Cross-Origin-Resource-Sharing genannt.

Die Backendseite kann das aber explizit erlauben, indem die richtigen Header gesetzt werden. Diese Aufgabe kann für uns das cors Paket übernehmen:

import cors from "cors"; import express from "express"; const app = express(); // Ab jetzt werden Requests von JEDER // Domain erlaubt und es ist auch erlaubt Cookies von jeder Domain // mit zu schicken! // Das sollte nur für die Entwicklung genutzt werden, // da hieraus Sicherheitsrisiken entstehen! app.use( cors({ origin: (origin, callback) => callback(null, origin), credentials: true, }) );

🎯 Ziel: Dein Frontend benutzt dein richtiges Backend

3.8. Cookies & Login

🎓 Wissen

Aktuell stehen wir vor einem Problem: HTTP ist Zustandslos. Wir wissen nicht wer welche Requests macht, welche Requests davor passiert sind und welche Requests in der Zukunft vielleicht noch kommen. Wir betrachten Anfragen bisher komplett isoliert.

Die Lösung: Wir weisen dem Nutzer einen eindeutigen Identifikator zu, den der Nutzer bei jeder Anfrage wieder mit schickt. Diesem Identifikator könnten wir dann beliebige Informatioenn zuordnen. Das können wir im Web mit Cookies implementieren. Express bietet dafür das express-session Paket:

import express from "express"; import session from "express-session"; const app = express(); // Zunächst müssen wir die session-middleware registrieren app.use( session({ // Das secret wird dazu benutzt, die Session zu signieren // So kann nicht einfach irgendein Nutzer seine eigenen // Session-IDs generieren. secret: "super secret passphrase", // Zeit in Millisekunden, wie lange der Cookie gültig ist. cookie: { maxAge: 1000 * 60 * 60 * 24 * 14 }, }) ); // Wir müssen express sagen, welche Felder // Auf einer session abgelegt werden sollen declare module "express-session" { interface SessionData { views: number | undefined; } } app.get("/page", (req, res) => { // Jetzt können wir die Felder auf session editieren // und können sicher sein, dass zukünftige Anfragen // Diese Session benutzen. if (!req.session.views) req.session.views = 0; req.session.views++; res.send(`${req.session.views} Views`); });

Achtung Cookies + CORS: Cookies und eine offene CORS-Einstellung sind generell nicht zu empfehlen, sind aber während der Entwicklung manchmal nötig. Hier ist darauf zu achten, dass im Frontend beim Laden der Daten mit angegeben werden muss, dass Cookies mitgeschickt werden sollen:

// Mit fetch export function getRandomQuote(): Promise<Quote> { return fetch(backendUrl + "/quotes/random", { // "Bitte Cookies mitschicken, auch wenn der // Request an eine andere Domain geht." credentials: "include", }).then((r) => r.json()); } // Mit Angulars HttpClient function getRandomQuote() { return this.client.get<Quote>(`${this.backendUrl}/quotes/random`, { withCredentials: true, }); }

Aufgabe 🎯 Ziel: Nur eingeloggte Nutzer können auf die Zitate zugreifen

4. Aufgabenstellung für Todo-App

4.1. Mit Datenbank vertraut machen.

Lade die Datenbank aus folgendem Link herunter: todos.db Sichte die Daten in der bereitgestellten Datenbank und versuche zu verstehen, wie die Daten zu interpretieren sind.

4.2. Backend-Projekt aufsetzen

Setze ein neues TypeScript Projekt auf, mit dem das Backend umgesetzt werden soll (siehe Setup Kapitel)

4.3. Mit Datenbank interagieren

Schreibe zunächst geeignete Interfaces, um Nutzer und Todos abzubilden. Achtung: SQLite hat keinen Datentyp für Booleans, deswegen wird meist ein Integer mit den Werten 0 bzw. 1 verwendet.

Implementiere die folgenden Funktionen, die mit der Datenbank interagieren:

4.4. HTTP-Backend aufsetzen

Implementiere die folgenden Endpunkte:

4.5. Sessions

In der Session soll die id des aktuellen Nutzers abgelegt werden.

Implementiere die folgenden Endpunkte:

4.6. Frontend aufsetzen

Setze ein neues Frontend-Projekt (React oder Angular) auf und implementiere eine Oberfläche für die Todo-App.