Express ist ein NodeJS Framework für Webapplikationen, welches sehr erweiterbar ist.
Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
(siehe http://expressjs.com/)
Express kann via NPM als Paket installiert werden:
npm install express
Als Beispiel kann ein einfacher Webserver mit ein paar Zeilen Code programmiert werden:
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
Der Webserver kann nach dem Start unter http://localhost:3000/ aufgerufen werden. Das Beispiel befindet sich in diesem Repository im Ordner 01_simple_webserver
. Der Webserver antwortet mit Hello World!
bzw. einem 404 Fehler (Cannot GET /test123
), wenn eine andere Seite aufgerufen wird.
Mit dem "Routing" gibt man dem Webserver die Information, welcher Programmcode bei einem bestimmten HTTP-Befehl ausfeführt werden soll. Express erlaubt eine flexible Konfiguration in diesem Bereich, womit recht einfach zusätzliche "Plugins" als Middleware (siehe weiter unten) für bestimmte Aufgaben einfach eingebunden werden können.
Man kann sich Routing als Regelwerk vorstellen, die dem Express-Server sagt, welche Funktion bei welchem HTTP-Aufruf ausgeführt werden soll:
GET / -> function seiteIndex(req, res) { ... }
GET /assets/katze.jpg -> static middleware
POST /api/login -> function apiLogin(req, res) { ... }
POST /neuerbeitrag -> function handlerFuerNeuenBeitrag(req, res) { ... }
PATCH /api/produkt -> function apiProductUpdate(req, res) { ... }
^ ^ ^
| | `----- Handler / Callback-Funktion
| `------------------------------------ Pfad; z.B. bei GET/POST das was in der Browseradresszeile nach dem Servernamen steht
`--------------------------------------------- HTTP-Verb
Ein herkömmlicher Webserver wie Apache oder nginx bildet üblicherweise das Dateisystem mit dem Pfad ab; wird beispielsweise http://localhost:3000/ordner1/datei1.txt aufgerufen, so wird üblicherweise die Datei datei1.txt
im Ordner ordner1
im Hauptverzeichnis der Webserverdateien (Konfigurationsabhängig) gesucht. Dieses Verhalten kann man natürlich auch in Express nachbilden (siehe static-Middleware weiter unten). Für unsere Anwendungszwecke werden wir die Routen für API-Endpoints meistens selbstständig konfigurieren.
Die Routenkonfiguration, also welcher Inhalt bei einer Adresse ausgegeben wird, erfolgt nach folgendem Prinzip (anhand des Codebeispieles oben):
app.METHOD(PATH, HANDLER)
^ ^ ^ ^
| | | `------ Callback Funktion, die ausgeführt wird, wenn die Route aufgerufen wird
| | `------------- Pfad (String, RegEx), der das HTTP-Ziel angibt, kann auch Parameter enthalten
| `------------------- HTTP-Verb (GET, PUT, POST, DELETE, ...)
`------------------------- Name der Express-Instanz
Der Handler (Callback Funktion) erhält 2 Argumente (request und response) und ist typischerweise eine Arrow-Function (=>
). Natürlich kann die Callback Funktion auch extra definiert und benannt werden, in der Praxis spart man sich diesen Schritt meistens bzw. übergibt den Webrequest einer eigenen Service-Schicht, die dann die Business-Logik vom Projekt übernimmt.
- Im request (üblicherweise
req
) Objekt befinden sich Informationen zum Aufruf (HTTP-Header vom Browser, Verschlüsselung etc.) - Im response (üblicherweise
res
) Objekt können wir Express sagen, wie mit diesem Aufruf zu verfahren ist (Text senden, JSON senden, eigene Header setzen etc.)
Ein POST-Handler würde beispielsweise so aussehen:
app.post('/', (req, res) => {
res.send('This is a POST request.')
})
Express kann beliebig erweitert werden, typischerweise wird dazu sog. Middleware eingesetzt. Eine Middleware schaltet sich, wie der Name vermuten lässt, zwischen Requestbearbeitung und der Antwort durch unseren Handler (z.B. app.get(...)
).
Ein Webrequest kann mehrere Middleware Schritte durchlaufen, bis sie zum eigentlichen Handler kommt oder in manchen Fällen (z.B. Prüfung ob User eingeloggt ist schlägt fehl) durch eine Middleware abgefangen wird.
Beispiel für eine Middlewarekonfiguration:
Express-Webrequest
`-> Cookie-Middleware: liest den Cookie-Header und speichert den Inhalt in req.cookies
`-> Logincheck-Middleware: prüft ob Pfad mit /api/ beginnt und User eingeloggt ist (z.B. Cookie)
`-> static files Middleware: prüft ob Pfad mit /assets/, sucht Datei und sendet diese Zurück
`-> (weitere Middleware)
`-> Handler: führt die Logik aus
Die Middleware kann mit dem Request alles machen, sie kann dem Benutzer auch statt unserem Handler antworten, so dass unser Handler gar nicht aufgerufen wird - dies ist beim Debuggen von Applikationen zu berücksichtigen. Man sollte sich daher bei Express-Applikationen zu Beginn einen Überblick über die verwendete Middleware und die Routes verschaffen, um zu verstehen, wie der Server Anfragen bearbeitet.
Ein typisches Szenario, wo wir eine Middleware einsetzen wollen, die einen Aufruf des eigenen Handlers verhindert, ist wenn wir verhindern wollen, dass ein User ohne Login gewisse Informationen abrufen kann. Unser Handler lagert die Prüfung an die Middleware aus und unberechtigte Anfragen werden z.B. mit einem HTTP 401 Unauthorized abgewiesen.
Ergänzend zum obigen Beispiel (Routing) würde sich unsere beispielhafte Middlewarekonfiguration folgendermaßen verhalten:
GET / -> durchläuft jede Middleware und landet beim Handler für die Hauptseite
GET /assets/katze.jpg -> durchläuft die Middlewares bis zur static files Middleware;
es wird nach der Datei am Dateisystem des Express-Servers gesucht und
an den Browser gesendet
POST /neuerbeitrag -> durchläuft cookie und logincheck Middleware
Wenn der User eingeloggt ist, werden weitere Middlewares durchlaufen
und der request an den Handler übergeben, der z.B. einen neuen Beitrag
in der Datenbank abspeichert
Wenn der User nicht eingeloggt ist, bricht die logincheck Middleware
die Verarbeitung ab und schickt eine Fehlermeldung, dass man nicht
eingeloggt ist
Eine Middleware wird in Express generell wie folgt eingebunden:
app.use(...);
als Beispiel dient die "static" Middleware von Express, hier kann das Verhalten eines normalen Webservers, also Dateien auszuliefern, in unserer Software nachprogrammiert werden. Im folgenden Beispiel werden vorhandene Dateien im Ordner public
und images
aufgerufen, wenn der Name mit der Route (path) übereinstimmt. Werden Dateien in der Route /cat
angefragt, beispielsweise /cat/cat1.jpg
, so wird im Ordner catpictures
danach gesucht.
Kommt eine Datei mehrmals vor, so nimmt Express den ersten Eintrag, der gefunden wird - hier beispielsweise public
, dann images
, dann catpictures
.
const path = require('path');
app.use('/static', express.static(path.join(__dirname, 'public'))); // Absolutpfad
app.use(express.static('images')); // relativ zum node Arbeitsverzeichnis
app.use('/cat', express.static('catpictures')); // route Prefix
Anmerkung: Der Pfad von express.static(...)
ist relativ vom Arbeitsverzeichnis von node, also dem Verzeichnis von wo das node Programm aufgerufen wird.
Eine Middleware kann auch leicht selber programmiert werden - beachte, dass next
explizit aufgerufen werden muss:
// Middleware NUR für api Routen (/api)
// es wird ein Spezialheader bei der Antwort an den Client (Browser) hinzugefügt
app.use('/api', function(req, res, next){
res.set('API-Header', 'wichtiger Header für die API Clients'); // eigener Code
next(); // ruft die nächste Middleware auf
});
// Middleware zum Mitloggen
// die Middleware schreibt bei einem Aufruf einfach logging something in die Konsole
const simpleLogger = function (req, res, next) {
console.log(`logging something`);
next();
}
// im Vergleich zu oben, wird diese Middleware bei ALLEN requests ausgeführt
app.use(simpleLogger);
// Middleware zum Mitloggen mit Prefix
// die Middleware schreibt bei einem Aufruf logging something mit einem Prefix in die Konsole
const advancedLogger = function(loggerPrefix) {
return function (req, res, next) {
console.log(`${loggerPrefix}: logging something`);
next();
}
}
app.use(advancedLogger('myprefix'));
Weitere nützliche Express-Middleware kann leicht im Internet gefunden werden, die folgende Liste soll einige Anregungen geben:
- body-parser
- compression
- cookie-parser
- cookie-session
- cors
- express-session
- helmet
- method-override
- morgan
- serve-favicon
- serve-index
Mit Template Engines können dynamische Inhalte in statische Templates (Vorlagen) eingefügt werden. Es können damit recht leicht vom express Server HTML-Seiten generiert werden. Wichtig ist zu verstehen, dass der dynamische Inhalt vom Server generiert wird und vom Server ein 'vorgerenderter' HTML Inhalt ausgeliefert wird; damit unterscheidet sich diese Technik fundamental von modernen Frameworks wie Vue/React/Angular wo dies vor allem am Client passiert (mit Ausnahmen).
Es stehen mehrere Template Engines zur Verfügung: pug, mustache, ejs etc.
Mehr dazu unter: http://expressjs.com/en/guide/using-template-engines.html
Guter Code sollte entsprechende Fehlerbehandlung vorsehen. In Express ist ein Standarderrorhandler bereits integriert, es ist aber in vielen Fäller nützlich ensprechende Anpassungen an die eigenen Bedürfnisse vorzunehmen.
Mehr dazu unter: http://expressjs.com/en/guide/error-handling.html
Es ist sehr beliebt Express-Applikationen beispielsweise als Microservice als Docker-Container zur Verfügung zu stellen. In der Regel ist die Applikation dann beispielsweise über localhost oder die Docker Container IP auf einem bestimmten Port ansprechbar. Um den Service in einen Webserver zu "integrieren" bieten sich sog. reverse proxies an.
Reverse Proxies leiten Anfragen (von einem Client idR außerhalb des Netzwerks) transparent an den Dienst weiter. Der Client bekommt von außen üblicherweise nichts davon mit. Ein reverse proxy ist damit Vergleichbar zu "NAT" aus der Netzwerktechnik.
Der wesentliche Vorteil liegt vor allem in der flexiblen Architektur, es gibt einen zentralen Server, der die Anfragen an die Subservices weiterreicht.
Express-Applikationen sollten für die Nutzung eines reverse proxy entsprechend konfiguriert werden, damit gewisse Informationen wie die originale IP-Adresse auch richtig verarbeitet werden können. Das geschiet technisch durch die zusätzlichen HTTP-Header X-Forwarded-Host
, X-Forwarded-Proto
, X-Forwarded-For
.
Mehr dazu unter: http://expressjs.com/en/guide/behind-proxies.html
Datenbanken können einfach in Express integriert werden, denn viele Datenbanken werden von NodeJS durch zusätzliche Module unterstützt. Neben den klassichen RDBMS wie MSSQL, MySQL werden auch zahlreiche NoSQL-Datenbanken wie MongoDB unterstützt.
Es gibt auch Zahlreiche Frameworks, die für die Datenbanken ein ORM anbieten.
Ziel dieses Kurses sind nur simple Backends, daher werden Datenbanken hier nicht weiter beschrieben.
Mehr dazu (mit vielen Beispielen) unter: http://expressjs.com/en/guide/database-integration.html
Express Applikationen können bei komplexeren Applikationen etwas verwirrend sein, daher kann man die Debugging-Informationen mit der Umgebungsvariable DEBUGGING
einfschalten. Falls requests lange dauern, kann mittels Debugging auch schnell ermittelt werden, welche Komponenten der Applikation besonder viel Zeit beanspruchen.
Das Programm wird dann wie folgt mit Debug-Informationen gestartet:
- Windows:
set DEBUG=express:* & node index.js
- Linux und macOS:
DEBUG=express:* node index.js
In vielen Situationen kann es dem Entwickler helfen, den Netzwerkverkehr der Applikation zu protokollieren. Am eigenen Computer ist das natürlich leicht mittels Wireshark zu bewerkstelligen. Auf remote Computern (z.B. einem Server) kann der Netzwerkverkehr beispielsweise mittels sshdump+Wireshark, tcpdump etc. aufgenommen werden.
Ein spezielles Problem stellt die Verschlüsselung dar; hier ist es zunächst sinnvoll zwei Szenarien zu unterscheiden:
client (browser) <--(1.)--> nodejs/express app <--(2.)--> remote server/api
- ein Client (Browser) fragt bei der Applikation via https an; hier bieten sich die Devtools im Browser an, vor allem die eingebauten Netzwerktools; es ist möglich http und https Traffic etc. unverschlüsselt zu sehen. Als Alternative kann man die Applikation auch unverschlüsselt laufen lassen und den Netzwerkverkehr mitprotokollieren.
- die Applikation fragt bei einem fremden Server nach zusätzlichen Daten um den request zu bearbeiten (z.B. Punktabfrage beim Moodleserver, Wetterdaten aus dem Internet etc.). Diese Daten werden idR verschlüsselt (https) übertragen und bei Fehlern hilft ein 'einfacher' Mitschnitt mittels Wireshark o.ä. nicht weiter - der Netzwerkverkehr ist verschlüsselt!
Eine Lösungsmöglichkeit wäre bspw. ein Proxy (+ Zertifikatscheck ausschalten), noch besser ist allerdings ein sog. TLS-Keylog. Dabei weist man Anwendungen mit TLS-Verschlüsselung an, die Schlüssel in eine Datei zu exportieren, damit man den Netzwerkverkehr mit Wireshark mitprotoollieren kann. Viele Anwendungen unterstützen das über die ENV-Variablen, NodeJS kann explizit mit folgendem Kommando angewiesen werden:
node --tls-keylog=/tmp/tlskeylog.txt
Nun kann man den mitprotokollierten Netzwerkverkehr in Wireshark entschlüsseln.
Wireshark Menü -> Einstellungen/Preferences -> Protocols -> TLS -> Dateipfad [z.B. /tmp/tlskeylog.txt] in (Pre)-Master-Secret log filename