© Shutterstock / wan wei
Gestatten, Node.js – Teil 4

Infrastruktur verwalten mit Node.js


Wer eine Anwendung in Node.js entwickelt, muss sich nicht nur mit dem Code an sich beschäftigen. Es gehört auch die Anbindung von Infrastruktur dazu, dazu zählen unter anderem Datenbanken, aber auch die Ausführungsebene mit Docker. Wie funktioniert das?

In den vergangenen drei Teilen dieser Serie ist eine in Node.js geschriebene Anwendung entstanden, die ein API zum Verwalten einer einfachen Aufgabenliste zur Verfügung stellt. Das API arbeitet dabei bewusst nicht nach REST, sondern trennt lediglich das Schreiben vom Lesen. Schreibende Vorgänge basieren auf POST, lesende auf GET. Die eigentliche Semantik wurde dabei in den Pfad des URL verlagert, sodass die fachliche Intention erhalten bleibt, und das API weitaus besser verständlich ist, als wenn es lediglich auf die vier in REST vorgesehenen technischen Verben setzen würde. Die aktuelle Ausbaustufe der Anwendung enthält drei Routen, zum einen zum Notieren und Abhaken von Aufgaben, zum anderen zum Auflisten aller unerledigten Aufgaben:

  • POST /note-todo

  • POST /tick-off-todo

  • GET /pending-todos

Das Trennen von schreibenden und lesenden Vorgängen setzt sich auch im Code fort, außerdem finden sich hier die fachlichen Bezeichnungen. Unterschieden wird zwischen Funktionen, die den Zustand der Anwendung verändern, und Funktionen, die den aktuellen Zustand auslesen und zurückgeben. Damit implementiert die Anwendung eine einfache Form des CQRS-Entwurfsmusters [1], das genau diese Trennung empfiehlt.

Von in-Memory zur Datenbank

Die Datenhaltung erfolgt derzeit RAM-basiert in einem Array. Den Kern stellt die Datei ./lib/Todos.js dar, in der die Klasse Todos implementiert wird. Ihr Konstruktur initialisiert ein Array, das anschließend durch die Funktionen noteTodo und tickOffTodo verändert beziehungsweise von getPendingTodos ausgelesen wird. Da die Funktionen bereits als async gekennzeichnet sind, ist es leicht, diese Klasse durch eine äquivalente Variante zu ersetzen, die auf eine Datenbank zugreift.

Dazu soll die Anwendung Plug-in-basiert erweitert werden, sodass sich beim Start entscheiden lässt, welche Datenhaltung verwendet wird. Hilfreich ist, dass von der Klasse Todos nur eine einzige Instanz in der gesamten Anwendung existiert, die sozusagen global in der Datei ./lib/getApp.js erzeugt und anschließend nur noch an andere Funktionen weitergereicht wird. Der erste Schritt besteht nun darin, die Klasse Todos in InMemoryTodos umzubenennen, die Datei ebenfalls umzubenennen und in das neue Verzeichnis ./lib/stores zu verschieben.

Als Nächstes muss die Datei ./lib/getApp.js angepasst werden, damit sie auf die neue Datei zugreift und die umbenannte Klasse instanziiert. Abgesehen vom Anpassen des Aufrufs der require-Funktion betrifft das aber nur eine einzige Zeile:

const todos = new InMemoryTodos();

Als Nächstes gilt es, dafür zu sorgen, dass der gewünschte Store von außen gewählt und konfiguriert werden kann. Damit alles Stores auf die gleiche Art erzeugt werden können, wird im Konstruktur von InMemoryTodos ein neues Objekt als Parameter eingeführt, mit dessen Hilfe beliebige Daten übergeben werden könnten. Da die Klasse InMemoryTodos keine Parameter erwartet, bleibt das Objekt an der Stelle leer, wie Listing 1 zeigt.

Listing 1

class InMemoryTodos { constructor ({}) { this.items = []; } // ... }

Auf den ersten Blick wirkt diese Änderung sinnlos, sie sorgt aber dafür, dass ein Store stets mit einem Objekt aufgerufen werden muss und keine Sonderbehandlung für Stores erforderlich ist, die keine Parameter erwarten. Der Aufruf in der Datei ./lib/getApp.js ändert sich dadurch wie folgt:

const todos = new InMemoryTodos({});

Die eigentlichen Parameter sollen aber nicht hart im Code kodiert werden. Spätestens bei einer echten Datenbank wäre das nicht sinnvoll, da man den Connection-String von außen ändern können möchte und es auch nicht ratsam ist, sensible Daten wie Zugangsdaten im Code zu hinterlegen. Stattdessen sollen die Parameter über die Umgebungsvariable STORE_OPTIONS gesetzt werden. Zusätzlich soll der Typ des Stores durch die Umgebungsvariable STORE_TYPE konfigurierbar sein. Als Standardwerte sollen der In-Memory-Store und ein leeres Objekt als Parameter dienen.

Dazu ist die Datei ./app.js anzupassen, in der bereits der Port aus einer Umgebungsvariablen ausgelesen wird. Besonders praktisch ist, dass das Modul processenv die Konvertierung des Inhalts der Umgebungsvariablen in den passenden Typ vornimmt, im Fall von STORE_OPTIONS in den Typ object. Anschließend müssen die beiden Parameter an die Funktion getApp übergeben werden (Listing 2).

Listing 2

'use strict'; const getApp = require('./lib/getApp'); const http = require('http'); const { processenv } = require('processenv'); (async () => { const port = processenv('PORT', 3_000); const storeType = processenv('STORE_TYPE', 'InMemory'); const storeOptions = processenv('STORE_OPTIONS', {}); const server = http.createServer(await getApp({ storeType, storeOptions })); server.listen(port); })();

Dort müssen die beiden Parameter e...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang