Wie man einen NodeJS Http Server erstellt
Inhaltsverzeichnis
- 1 Grundprinzipien eines (NodeJS) Http Servers
- 2 Das simpelste Beispiel
- 3 Erweitertes NodeJS Http Server Beispiel im Selbstversuch
- 4 Du willst mehr!?
- 5 Downloads
- 6 Weiterführende Links
Wie Du einen NodeJS Http Server erstellen kannst, erfährst Du im heutigen Beitrag.
Ich verzichte der Einfachheit halber und aufgrund der meiner Meinung nach sinnvollen Trennung auf weitere Frameworks.
Für den Bau eines NodeJS Http Servers werden wir also nur die bereits vorhandenen Bordmittel von Node verwenden.
Grundprinzipien eines (NodeJS) Http Servers
Bevor wir in den Code und Weiteres starten würde ich sagen, dass wir uns erstmal den Basics des Webs widmen.
Vielleicht ist Dir das Grundprinzip von Webservern bereits bekannt, dann bist Du natürlich herzlich eingeladen diesen Abschnitt zu überspringen.
Für alle Anderen und die somit eventuellen Neulinge unter den Besuchern, schauen wir uns aber den Ablauf eines Webservers nochmal an.
Das Grundprinzip lässt sich auf viele Bereich übertragen, also nicht nur auf das simple „Serven“ (Bereitstellen) einer Datei auf Anfrage.
Wenn Du Dir das Bild anschaust, siehst Du eine (oder mehrere) typische Situatione(n).
Ablauf einer Anfrage
Links befinden sich die verwendeten Geräte, wie z. B. Laptops & Desktop PCs, Smartphones und Tablets.
Diese rufen z. B. in Ihrem Browser eine bestimmte Website/Adresse auf, Sie senden also eine Anfrage an den Server.
Danach schaut der Server nach seiner entsprechenden Logik, um die jeweilige Anfrage zu beantworten.
Dabei sendet „er“ verschiedene Daten in seiner Antwort wie z. B. den Kopf, oder den Body.
Der Inhalt der Antwort hängt natürlich unter Anderem stark von der Gestaltung des jeweiligen „Endpunktes“ und der allgemeinen Server-Infrastruktur ab.
Handelt es sich dabei um eine Datei sieht die Antwort anders aus, als wenn man nur über eine Schnittstelle diverse Kunden abrufen möchte.
Das simpelste Beispiel
Ich denke es ist an der Zeit, für ein wirklich (wirklich) simples Beispiel eines Node Http Servers.
Schauen wir uns erstmal an, was wir aus der Standard-Bibliothek von Node (Module) verwenden können.
Wer sucht wird relativ schnell fündig und stößt auf das gute alte „Http“-Modul – wer hätte es gedacht.
Das Http-Modul
Um das „Http“-Modul zu „importieren“ und verwenden zu können, legen wir wie üblich eine Konstante mit jeweiligem Namen an.
Des Weiteren brauchen wir noch die für Node Programme typische „require„-Funktion, welche das Modul letztendlich importiert und als Rückgabewert zurückgibt.
Solche Module können wir natürlich auch selbst schreiben, allerdings ist das ein Thema für einen späteren Beitrag!
Legen wir also nun die Konstante an und verwenden Diese anschließend:
const http = require('http')
Natürlich verwende ich hier eine Konstante, statt einer normalen Variable (var) oder „scoped“ Variable (let), da sich der dort drin stehende Wert nicht verändern wird/soll!
Der Request-Handler
Danach erstellen wir einen Request-Handler, Welcher jeweils einen Request (eine Anfrage an den Server) verarbeiten wird.
Dieser Handler ist letztendlich nichts anderes als eine kleine Funktion, Welche 2 Parameter mitgeliefert bekommt – der Request und die Response.
Damit bekommen wir die Möglichkeit Daten vom Request zu lesen, bzw. Daten in die Response zu schreiben.
const requestHandler = function (req, res) { res.writeHead(200, { 'Content-Type': 'application/json' }) res.write("NodeJS Http Server") res.end() }
Server erstellen und ausführen
Im letzten Schritt des einfachen Beispiels müssen wir nur noch einen „Server“ mit Hilfe der „createServer„-Funktion des „Http“-Moduls erstellen.
Danach können wir dem Server einfach nur noch sagen: „Hör‘ zu!“ und schon läuft das Ding!
Wir legen zuerst zwei Konstanten fest, Welche eine kleine Art Konfiguration widerspiegeln.
Dabei sagen wir dem Programm, auf welchen Port bzw. auf welchen Host–Namen es lauschen soll:
const port = 8000 const host = 'localhost'
Nun erstellen wir den Server indem wir einfach wie angekündigt die „createServer“-Funktion des „Http“-Moduls aufrufen.
Dann rufen wir nur noch die „listen„-Funktion auf und schön „hört“ unser Server auf ankommende Anfragen.
Um eine Art Status auszugeben, geben wir der „listen“-Funktion noch ein anonymes Callback mit, Welches bei Start des Servers aufgerufen wird.
Hier könnten wir natürlich auch ähnlich wie mit dem „Request-Handler“ verfahren und eine benannte Funktion, statt einer Anonymen verwenden..
const server = http.createServer(requestHandler) server.listen(port, host, function () { console.log(`Listening for requests on https://${host}:${port}`) })
Nun kannst Du die Anwendung z. B. in Visual Studio Code über die Konsole ausführen (oder Du verwendest die normale Windows/Mac Konsole/Terminal).
Öffne in Visual Studio Code ein Terminal mit dem Hotkey „Strg+Shift+ö“, oder gehe oben über’s Menü.
Dann musst Du den obigen Code nur noch in eine JavaScript-Datei, z. B. „index.js“ stecken und mit „node index.js“ (geht auch ohne „.js“) ausführen.
Wenn Du dann die konfigurierte Adresse „https://localhost:8000“ besuchst, dürfest Du folgende Ausgabe sehen:
Fazit
Nun haben wir das simple Beispiel abgeschlossen, jedoch kommen in Zukunft noch weitere Problematiken auf uns zu.
Diese kommen unter Anderem dann, wenn man mit einzelnen Routen/Endpunkten und auch z. B. einer Authentifizierung arbeiten möchte.
Mit dem aktuellen Stand könnten wir also keine wirklich sinnvolle, bzw. eher vielfältige Applikation bauen – zumindest nicht so easy..
Das aktuelle Skript arbeitet praktisch als „Catch-All-Route„, es arbeitet also jeden GET-Request bisher gleich ab.
Egal ob wir „<adresse>/kunden“, oder „<adresse>/dashboard“ aufrufen, es kommt immer das Gleiche als Antwort.
Das liegt natürlich daran, dass wir in dem obigen Skript in keiner Weise zwischen verschiedenen Endpunkten zu unterscheiden.
Simpler NodeJS Http Server Code
const http = require('http') const requestHandler = function (req, res) { res.writeHead(200, { 'Content-Type': 'application/json' }) res.write('NodeJS Http Server') res.end() } const port = 8000 const host = 'localhost' const server = http.createServer(requestHandler) server.listen(port, host, function () { console.log(`Listening for requests on https://${host}:${port}`) })
Erweitertes NodeJS Http Server Beispiel im Selbstversuch
Nachdem wir nun oben schon einige Fallstricke feststellen konnten, schauen wir uns im nächsten Abschnitt mal einen kleinen Selbstversuch an.
Wir schreiben uns eine eigene kleine Server–Klasse, mit dessen Hilfe wir verschiedene Request-Arten und Routen registrieren können.
Wie Dir vielleicht bekannt ist, gibt es nicht nur die oben verwendete Http „GET“-Methode, sondern z. B. noch „POST“, „PUT“, „DELETE“ und mehr.
Ein Beispiel wäre ein „GET„-Request an den „/kunden“-Endpunkt, Welcher z. B. eine Suche signalisiert und eventuell noch Query-Parameter zur Filterung a la „?name=Max“ entgegennehmen kann.
Wenn man nun allerdings ein „POST“-Request an den „/kunden“-Endpunkt schickt, könnte man durch die Angabe passender Daten z. B. einen Kunden erstellen.
Los geht’s – die jetzt benötigten Module
Bei dem erweiterten Beispiel-Server benötigen wir im Vergleich zu vorhin einige zusätzliche Module.
Wir möchten natürlich so viel Arbeit wie nur möglich an die Core-Module von NodeJS abgeben.
Darunter fällt zum Beispiel das Verarbeiten, also das Parsen des Url-Strings, bestehend aus Endpunkt und Query–Parameter.
const http = require('http'), url = require('url'), querystring = require('querystring')
Die Server-Klasse
Im Optimalfall stellt man sich die Konfiguration des Servers meiner Meinung nach so aktuell am besten vor:
- Server instanziieren
- Routen definieren
- Server lauschen lassen und Handler die Arbeit machen lassen
Daher werde ich den Server nach Möglichkeit genau so versuchen in kurzer Zeit zu bauen – damit es nicht den Rahmen sprengt.
Selbstverständlich ist Deine eigene Kreativität gerne gesehen und keine Grenzen in Sicht, „do your thing“ sag ich mal!
Definieren wir also im ersten Schritt eine simple JavaScript-Klasse mit unserem gewünschten „Skelett“-Aufbau:
class Server { constructor () { // doin' some constructional things } get () { // registering a GET route.. } post () { // registering a POST route.. } start () { // starting the server making it listening to requests } }
Der Konstruktor
Im Konstruktor des Servers übergeben wir die Haupt-Konfiguration, wie den Hostname und den Port.
Ggf. könnte man es auch hier mit einem „options“-Objekt gestalten, allerdings ist das für’s Erste nicht notwendig.
Meistens bietet es sich aber tatsächlich an, da die Konfigurationsmöglichkeiten häufig von Zeit zu Zeit wachsen und man natürlich sowas wie 7 Konstruktor-Parameter vermeiden sollte.
Zuerst kümmern wir uns um Auflistung, Welche später die einzelnen registrierten Routen enthalten wird.
Ein Workaround
Ich habe hier bewusst eine anonyme Funktion mit „Arrow-Function“-Deklaration gewählt, um den „This-Scope“ des Konstruktors zu behalten.
So komme ich kinderleicht an das „_routes“ Feld/Eigenschaft – bau‘ dies gern nach Belieben um..
Würde ich hingegen eine weitere Methode in der Klasse hinzufügen, würde ich innerhalb der Methode den Scope des „http“-Moduls haben (und somit nicht an die Routes kommen..).
Nachdem die „createServer„-Funktion ihr Ding macht, nehmen wir noch Standardwerte für die Konfiguration (host, port) auf und setzen Sie ansonsten wie übergeben.
Vorgehensweise
Das Callback der „createServer“-Funktion loggt im ersten Schritt die besuchte Url inkl. Route, damit wir verfolgen können was passiert.
Danach verarbeiten wir mit einer der oben angesprochenen Utility-Module die Request–Url.
Somit können wir dann kinderleicht an den Pfad kommen (z. B. „/customers“) und müssen hier nicht nach dem Query-String schauen.
Dieser kommt zu einem späteren Zeitpunkt noch bei z. B. der Filterung zum Einsatz.
Ansonsten machen wir das, was viele Server machen, wenn Sie eine Url nicht finden können.
Sie melden einen 404-Fehler und wir verlassen dann den Durchlauf mit einem „Early Return„!
Falls die Route jedoch gefunden wird, sich also eine zum Pfad und zur Request-Methode passende Route in unserer Auflistung befindet, ziehen wir Diese.
Zum Schluss rufen wir dann den dafür registrierten Handler der Route auf und übergeben das Request- & Response-Objekt als Parameter.
So können die Handler entscheiden, was Sie mit den dem jeweiligen Objekt machen und der zentralen Stelle oben obliegt einzig und allein die Delegation – schön!
// .. constructor(host, port) { const routes = [] this._routes = routes this._server = http.createServer((req, res) => { console.log(`[${req.method}] ${req.url}`) const requestUrl = url.parse(req.url) const route = routes.find(x => x.path === requestUrl.pathname && x.method === req.method) if (!route) { res.writeHead(404, { 'Content-Type': 'application/json' }) res.write('Route not found') res.end() return } route.handler(req, res) }) this._host = host || 'localhost' this._port = port || 8000 } // ..
Ein paar Hilfsmethoden
Normalerweise bin ich kein Fan von komplexen Abfang-Szenarien in Beispiel-Codes, allerdings dürfte das hier relativ überschaubar sein, daher..
Wir definieren 3 Methoden, Welche die Überprüfung der notwendigen Argumente/Parameter bei einer Routen-Registrierung übernehmen.
Man hat ja schließlich keine Lust (und es ist schlechter Coding-Stil), alles 5 mal zu schreiben – duplicated Code, bah!
_checkPath-Methode
Nun gibt es also die „_checkPath„-Methode, Welche einfach nur prüft, ob das übergebene Argument „ok“ ist, also nicht „leer“ und vom Typ String.
Zusätzlich prüft die Methode noch, ob schonmal eine gleiche Route registriert wurde (unabhängig vom Handler..).
_checkHandler-Methode
Die „_checkHandler“-Methode prüft auch ob überhaupt etwas übergeben wurde und zusätzlich, ob es sich dabei auch um eine gewünschte Funktion handelt.
_registerRoute-Methode
Als nächstes kommt das Routen-Helferlein zum Einsatz, Welches uns – wer hätte es gedacht.. – bei der Registrierung einer Route unterstützt.
Auch hier gilt wieder die Vermeidung von duplicated Code, etc..
Es wird ein neues Route-Objekt mit Hilfe der angegebenen Parameter erstellt und der Auflistung hinzugefügt.
_checkPath(path, method) { if (!path || typeof path !== 'string') { throw new Error('No or invalid path specified') } const existentRoute = this._routes.find(x => x.path === path && x.method === method) if (existentRoute) { throw new Error(`Route already registered [${method}] ${path}`) } } _checkHandler(handler) { if (!handler || typeof handler !== 'function') { throw new Error('No or invalid handler specified') } } _registerRoute(path, method, handler) { const route = { path, method, handler, } this._routes.push(route) }
Routen registrieren
Im Folgenden Code werden zwei Möglichkeiten einer Routen-Registrierung definiert:
- GET-Routen
- POST-Routen
get(path, handler) { this._checkPath(path, 'GET') this._checkHandler(handler) this._registerRoute(path, 'GET', handler) } post(path, handler) { this._checkPath(path, 'POST') this._checkHandler(handler) this._registerRoute(path, 'POST', handler) }
Dabei wird bis auf die Request-Methode nicht wirklich etwas anderes gemacht, könnte man ggf. auch anders gestalten, aber cmon..
Den Server starten
Um den Server zu starten, müssen wir auf das klasseninterne Feld namens „_server“ zugreifen und dessen „listen„-Methode aufrufen.
Ggf. könnte man hier noch mit irgendwelchen Zustands-Flags für einen noch zu implementierenden Stop realisieren.
Kommen wir nun zu einer Beispiel-Konfiguration und einer Art Kunden-API.
Dazu erstelle ich eine (nicht persistente, nur im Server-Memory existierende) Kunden-Auflistung.
Als nächstes instanziiere ich wie oben geplant eine neue Server–Instanz.
Im Anschluss werden die Routen registriert und der Server durch die „start“-Methode gestartet.
Die Such-Route für die Kunden
Die erste zu registrierende Route wäre die Such-Route für die Kunden (bzw. die Schnittstelle..).
Pro Anfrage entsteht im ersten Schritt ein neues Array mit dem Inhalt der ursprünglichen „customers“-Auflistung.
Dann führen wir ein paar Prüfungen durch, ob es wirklich eine Suche ist, also ob wirklich Suchparameter übergeben wurden.
Falls ja filtern wie die „responseCustomers“ und weisen der gleichen Variable das neue Ergebnis zu.
So könnte man Schritt für Schritt natürlich weitere Such-Möglichkeiten hinzufügen.
Zum Schluss geben wir die Daten JSON-konform an den aufrufenden Client aus – fertig!
Gerne darfst Du Dich daran versuchen, die fehlende POST-Route zu bauen *zwinker*.
const customers = [ { company: 'Nice Company ltd.' }, { company: 'Another Company' }, ] const server = new Server() server.get('/customers', (req, res) => { let responseCustomers = [...customers] const shouldFilter = req.url.includes('?') && req.url.includes('=') if (shouldFilter) { const requestUrl = url.parse(req.url) const query = querystring.parse(requestUrl.query) const hasNameFilter = 'name' in query if (hasNameFilter) { const nameToFilter = (query['name'] || '').toLowerCase() if (nameToFilter) { responseCustomers = responseCustomers.filter(x => x.company.toLowerCase().includes(nameToFilter)) } } } res.writeHead(200, { 'Content-Type': 'application/json' }) res.write(JSON.stringify(responseCustomers)) res.end() }) server.post('/customers', (req, res) => { // to be done.. }) server.start()
Erweiterter NodeJS Http Server Code
const http = require('http'), url = require('url'), querystring = require('querystring') class Server { constructor(host, port) { this._routes = [] this._server = http.createServer((req, res) => { console.log(`[${req.method}] ${req.url}`) const requestUrl = url.parse(req.url) const route = this._routes.find(x => x.path === requestUrl.pathname && x.method === req.method) if (!route) { res.writeHead(404, { 'Content-Type': 'application/json' }) res.write('Route not found') res.end() return } route.handler(req, res) }) this._host = host || 'localhost' this._port = port || 8000 } _checkPath(path, method) { if (!path || typeof path !== 'string') { throw new Error('No or invalid path specified') } const existentRoute = this._routes.find(x => x.path === path && x.method === method) if (existentRoute) { throw new Error(`Route already registered [${method}] ${path}`) } } _checkHandler(handler) { if (!handler || typeof handler !== 'function') { throw new Error('No or invalid handler specified') } } _registerRoute(path, method, handler) { const route = { path, method, handler, } this._routes.push(route) } get(path, handler) { this._checkPath(path, 'GET') this._checkHandler(handler) this._registerRoute(path, 'GET', handler) } post(path, handler) { this._checkPath(path, 'POST') this._checkHandler(handler) this._registerRoute(path, 'POST', handler) } start() { this._server.listen(this._port, this._host, () => { console.log(`Listening for requests on https://${this._host}:${this._port}`) }) } } const customers = [ { company: 'Nice Company ltd.' }, { company: 'Another Company' }, ] const server = new Server() server.get('/customers', (req, res) => { let responseCustomers = [...customers] const shouldFilter = req.url.includes('?') && req.url.includes('=') if (shouldFilter) { const requestUrl = url.parse(req.url) const query = querystring.parse(requestUrl.query) const hasNameFilter = 'name' in query if (hasNameFilter) { const nameToFilter = (query['name'] || '').toLowerCase() if (nameToFilter) { responseCustomers = responseCustomers.filter(x => x.company.toLowerCase().includes(nameToFilter)) } } } res.writeHead(200, { 'Content-Type': 'application/json' }) res.write(JSON.stringify(responseCustomers)) res.end() }) server.post('/customers', (req, res) => { }) server.start()
Du willst mehr!?
Wenn Du mehr willst, sprich Deine Anwendung wirklich komplex gestalten möchtest, kommst Du nicht um das non plus ultra Framework herum.
Dabei handelt es sich um das in Node-Kreisen sehr bekannte ExpressJS Framework.
Neben gewisse Basis-Funktionen wie die einfache Definition von Routen unterstützt ExpressJS auch noch ein Konzept namens „Middlewares„.
Hierüber werde ich allerdings noch in einem getrennten Beitrag schreiben, da es sonst zu viel wird!