Werkzeugunterstützung in Node.js sinnvoll nutzen

8 hilfreiche Tipps zur Umsetzung von Node.js-Architekturen

Sebastian Springer

© Shutterstock / 32 pixels

Es gibt viele gute Gründe, warum man sich vor und während der Entwicklung einer Node.js-Applikation Gedanken über die Architektur machen sollte. Einer der wichtigsten Gründe ist, dass stetig neue Anforderungen an die eigene Applikation gestellt werden und sich diese im Lauf der Zeit auch ändern können. Deshalb sollte die Applikation mithilfe einer guten Node.js-Architektur über ein gewisses Maß an Flexibilität verfügen.

Nicht nur aus Anforderungssicht ist eine stabile und konsistente Architektur in einer Node.js-Applikation wichtig, sie hilft auch beim Umgang mit dem stetigen Wandel, dem Sie in der JavaScript-Welt gegenüberstehen. In diesem Artikel lernen Sie einige Aspekte von Node.js-Architekturen kennen, die Sie in Ihrer Applikation anwenden können, um Ihren Arbeitsalltag zu erleichtern.

Allgemein: Werkzeugunterstützung nutzen

Für zahlreiche Problemstellungen in der Entwicklung von Node.js-Applikationen gibt es mittlerweile einige Werkzeuge, die Sie bei der Arbeit unterstützen. Generell sollten Sie allerdings darauf achten, dass Sie sich wiederholende Aufgaben möglichst durch den Einsatz von Werkzeugen oder Bibliotheken automatisieren. Das spart Zeit und reduziert Fehlerpotenzial.

Ein Beispiel für ein solches Werkzeug ist ESLint, ein Werkzeug zur statischen Codeanalyse für JavaScript. ESLint lässt sich mit wenigen Handgriffen in ein Projekt einbinden und dann sowohl als Aktion im Build-Prozess der Applikation als auch direkt im Entwicklungsprozess als Plug-in der Entwicklungsumgebung integrieren. Einigen Sie sich im Projektteam auf einen Codestandard wie ihn beispielsweise der Airbnb Style Guide vorgibt, können Sie ESLint dazu verwenden, die Einhaltung des Codestandards sicherzustellen. Es gibt einige weitere Aspekte, die zu einer guten Node.js-Architektur beitragen – acht hilfreiche Tipps und Tricks habe ich zusammengetragen.

1. Modularisierung

Node.js verfügt über ein Modulsystem, mit dem Sie Ihre Applikation in kleinere Einheiten strukturieren können. Damit lässt sich das Single-Responsibility-Prinzip umsetzen. Es besagt, dass jedes Modul und jede Klasse nur eine bestimmte Verantwortung hat, sich also nur um eine bestimmte Sache kümmern soll. Halten Sie dieses Prinzip ein, komponieren Sie Ihre Applikation aus einer Menge kleinerer Bausteine. Diese sollten untereinander in einer möglichst losen Verbindung stehen, um die Unabhängigkeit sicherzustellen. Bauen Sie Ihre Applikation nach diesem Prinzip auf, haben Sie die Möglichkeit, einzelne Module unabhängig von der Gesamtapplikation zu modernisieren oder auszutauschen.

Schon in den ersten Versionen von Node.js wurde ein solcher modularer Aufbau durch das Modulsystem unterstützt. Ursprünglich kam das CommonJS-Modulsystem mit der require-Funktion für den Import von Modulen und dem module.exports-Konstrukt zum Exportieren von Schnittstellen zum Einsatz. Seit einigen Versionen wird auch das ECMAScript-Modulsystem unterstützt. Dieses ist zunächst standardmäßig deaktiviert und muss von Ihnen durch die Option –experimental-modules aktiviert werden. Das Modulsystem arbeitet mit den Schlüsselworten import zum Laden von Modulen und export zur Definition von Schnittstellen. Die Dateien, die das ECMAScript-Modulsystem verwenden, müssen auf .mjs statt auf .js enden. Berücksichtigen Sie das nicht, erhalten Sie eine Fehlermeldung.

Im Gegensatz zu clientseitigem JavaScript müssen Sie sich bei Node.js nicht um Bundling oder die Optimierung von Quellcode kümmern, da der Quellcode auf dem Server zur Verfügung steht, wo er auch ausgeführt wird; der Transfer des Codes über eine Netzwerkverbindung entfällt demnach. Das Node.js-Modulsystem weist außerdem einige Optimierungen auf, die dafür sorgen, dass durch eine Aufteilung in eine Vielzahl kleiner Dateien keine Performancenachteile entstehen. Ein Teil dieser Optimierungen stammt direkt aus dem Kern von Node.js. Beim Start der Applikation werden alle direkt erreichbaren Importe evaluiert, die Dateien von der JavaScript-Engine eingelesen und in Bytecode umgewandelt. Das führt allerdings dazu, dass Sie während des Entwicklungsprozesses den Node.js-Prozess neu starten müssen, damit die Änderungen wirksam werden.

Eine weitere Optimierung betrifft das Caching-Verhalten bei Modulen. Binden Sie ein Modul über import oder require ein, wird der Code ausgeführt. Das führt dazu, dass sämtliche Seiteneffekte wie beispielsweise Funktionsaufrufe direkt im Modul wirksam werden. Das Ergebnis der Modulausführung, also die öffentliche Schnittstelle, wird dann in den Modulcache geschrieben. Binden Sie dann das Modul ein zweites Mal an einer anderen Stelle in Ihrer Applikation ein, wird das Modul nicht erneut ausgeführt, sondern das Ergebnis aus dem Cache verwendet. Das führt dazu, dass keinerlei Seiteneffekte mehr auftreten. Statt den Modulcache zu leeren, sollten Sie für gewollte Seiteneffekte Funktionen exportieren und diese explizit ausführen, da Ihr Quellcode damit besser lesbar wird (Listing 1).

import {add} from './calculator';

export const summarize = (data) => data.reduce(
  (prev, current) => add(prev, current), 
  0);

2. Verzeichnisstruktur

Direkt mit der Modularisierung einer Applikation hängt die Strukturierung des Quellcodes im Dateisystem zusammen. In diesem Zusammenhang haben sich in den vergangenen Jahren einige Erfolgsmodelle herausgebildet, die allesamt das Ziel verfolgen, eine Applikation übersichtlich zu halten, auch wenn der Funktionsumfang stetig wächst. Allgemein sollten Sie mit einer möglichst flachen und einfachen Verzeichnisstruktur beginnen und diese erst bei Bedarf erweitern. Zur Strukturierung empfiehlt sich bei kleinen Applikationen mit wenig voneinander abgegrenzten Fachlichkeiten, die Dateien nach ihrem Zweck zu strukturieren. Bauen Sie Ihre Webapplikation beispielsweise nach dem MVC-Pattern auf, erzeugen Sie Verzeichnisse für Ihre Models, Views und Controller und sortieren dort die jeweiligen Dateien ein. Zusätzlich bietet es sich an, ein shared-Verzeichnis zu erstellen, in dem Sie Hilfskonstrukte ablegen können.

Weist Ihre Applikation einen größeren Funktionsumfang auf, bei dem Sie die einzelnen Fachlichkeiten deutlicher voneinander abgrenzen können, nutzen Sie dies als Strukturierungsmerkmal. Jeden Bereich bilden Sie dann durch ein eigenständiges Verzeichnis ab. So könnte beispielsweise die Benutzerverwaltung in einem user-Verzeichnis untergebracht werden. Je nach Umfang des Moduls können Sie zusätzliche Untermodule bilden oder die Dateien wieder nach ihrer technischen Struktur gruppieren.

Bei den Dateinamen sollten Sie auf Großschreibung verzichten und die Dateien nur mit Kleinbuchstaben benennen. Eine Alternative zur weit verbreiteten CamelCase-Schreibweise ist die Kebab-Case-Schreibweise, bei der die einzelnen Wörter nicht durch große Anfangsbuchstaben, sondern durch Bindestriche voneinander getrennt werden. Damit umgehen Sie Probleme mit Dateisystemen, die nicht case-sensitiv sind.

Unterteilen Sie Ihre Applikation in verschiedene Module, können Sie auf ein weiteres Feature von Node.js im Zusammenhang mit dem Modulsystem zurückgreifen. Weist ein Verzeichnis eine Datei mit dem Namen index.js auf, wird diese standardmäßig als Einstiegsdatei für dieses Verzeichnis verwendet. Importieren Sie also das Verzeichnis, wird die index.js-Datei geladen. In dieser können Sie dann gesammelt alle Schnittstellen des Moduls exportieren. Das hat den Vorteil, dass Sie bei der Verwendung eines Moduls nicht mehr jede einzelne Datei und ihre Schnittstellen kennen müssen, sondern nur noch eine zentrale Export-Datei pro Modul haben.

3. Asynchronität

Arbeiten Sie mit Node.js, werden Sie schnell mit asynchronen Operationen konfrontiert. Die Anbindung von Fremdsystemen sowie der Zugriff auf Systemressourcen erfolgt in der Regel asynchron. Das bedeutet, dass Sie eine Operation ausführen und keinen direkten Zugriff auf ihren Rückgabewert haben. Dieses Merkmal beeinflusst den Aufbau des Quellcodes und die Kommunikation innerhalb einer Applikation stark. Zum Umgang mit Asynchronität stehen Ihnen eine Reihe von Möglichkeiten zur Verfügung. Die Frage, die Sie als erstes beantworten müssen, ist, ob es sich um eine Operation mit nur einem Ergebnis oder einen kontinuierlichen Fluss von Ergebnissen handelt. Im ersten Fall können Sie auf Callback-Funktionen oder Promises zurückgreifen. Im zweiten Fall kommen in der Regel Streams zum Einsatz. Streams und die ihnen zugrunde liegenden EventEmitter werden auch zur Kommunikation innerhalb von Applikationen und mit Fremdsystemen eingesetzt. Mit diesem Aspekt der Architektur befasst sich der nächste Abschnitt.

Die einfachste Lösung für eine asynchrone Operation ist der Einsatz von Callback-Funktionen. Das sind Funktionsobjekte, die einer Funktion übergeben und nach der Beendigung der asynchronen Operation ausgeführt werden. Grundsätzlich können Sie diese Callbacks frei gestalten, jedoch hat sich in Node.js eine Konvention für die Gestaltung von Callbacks herausgebildet. Das erste Argument, das Sie der Callback-Funktion übergeben, repräsentiert einen Fehler. Im Erfolgsfall übergeben Sie den Wert null. Alle weiteren Argumente stellen die Ergebnisse der Operation dar. Tritt ein Fehler auf, übergeben Sie als erstes Argument eine Objektrepräsentation des Fehlers und die Callback-Funktion muss diesen entsprechend behandeln. In diesem Fall sollten Sie den Fehler auf jeden Fall protokollieren, damit ein gehäuftes Auftreten solcher Ausnahmesituationen analysiert werden kann. Anschließend können Sie den Fehler entweder direkt behandeln oder das Fehlerobjekt mit einem throw-Statement weiterwerfen. Wird der Fehler an keiner Stelle behandelt, wird die Applikation terminiert.

Lange Zeit waren Callbacks die einzige Möglichkeit, um mit Asynchronität umzugehen. Sie weisen jedoch gerade bei der Flusssteuerung erhebliche Nachteile auf. Haben Sie voneinander abhängige Operationen oder möchten Sie parallel mehrere Operationen abarbeiten und die Ausführung Ihrer Applikation erst fortsetzen, wenn alle Operationen erfolgreich beendet sind, benötigen Sie umständliche Hilfskonstrukte. Eine elegantere Lösung bieten an dieser Stelle Promises. Das sind Objekte, an deren Erfüllung Sie Callback-Funktionen binden können. Neben der Callback-Funktion für den Erfolgsfall können Sie eine zweite Funktion für die Fehlerbehandlung angeben. Sie sehen also, dass Sie mit dem Einsatz von Promises nicht weniger, sondern mehr Callbacks schreiben müssen. Der Vorteil von Promises ist, dass Sie für eine Verkettung asynchroner Operationen Promises nicht ineinander schachteln, wie es bei Callbacks üblich ist, sondern diese hintereinander hängen, ähnlich einer Kette. Die Fehlerbehandlung kann in diesem Fall entweder individuell für jede Operation erfolgen oder gesammelt am Ende der Kette. Tritt in einer Promise ein Fehler auf, wird der Fehler durch die Kette gereicht, bis eine Fehlerbehandlungsroutine gefunden ist. Ist das nicht der Fall, wirkt der nicht behandelte Fehler wie eine Exception, die zur Beendigung Ihrer Applikation führt.

Node.js integriert in regelmäßigen Abständen neue Versionen der V8-Engine, also der zentralen JavaScript-Engine der Plattform. Damit werden auch immer wieder neue JavaScript-Sprachfeatures verfügbar. Im Umgang mit Asynchronität ist hier vor allem das async/await-Feature zu erwähnen (Listing 2). Mit dem await-Schlüsselwort können Sie auf die Erfüllung eines Promise-Objekts warten und sparen sich somit die Callback-Funktion. Auch die Fehlerbehandlung wird vereinfacht, da Sie einen Fehler durch ein einfaches try-catch-Statement fangen und behandeln können.

Voraussetzung für die Verwendbarkeit von await ist, dass es innerhalb einer mit async markierten Funktion verwendet wird. Dieses Schlüsselwort hat zur Folge, dass die Funktion selbst ein Promise-Objekt zurückgibt, in das der Rückgabewert der Funktion gewrappt wird. Async-Funktionen und Promises lassen sich sehr gut kombinieren. So können Sie den Rückgabewert einer async-Funktion mit Promise.all und Promise.race nutzen und mehrere asynchrone Operationen parallel ausführen oder nur mit der frühesten Antwort fortfahren.

export const fetchValues = async url => {
  const data = await fetchFromServer(url);
  // do calculation
  return result;
};

4. Kommunikation

Wie bereits erwähnt, kommen Promises vor allem dann zum Einsatz, wenn die asynchrone Operation lediglich ein Ergebnis liefert. Erwarten Sie mehr als ein Ergebnis, benötigen Sie ein Event-System, ähnlich wie die Event Handler, die im Frontend-seitigen JavaScript zum Einsatz kommen. Zahlreiche Kernmodule wie die HTTP-Module von Node.js basieren selbst auf einem Event-System. Die Basis bildet das events-Modul. Es exportiert die Klasse EventEmitter, die unter anderem die Methoden emit zum Auslösen von Events und on zum Subscriben auf Events bietet. Für einfache Problemstellungen reicht ein EventEmitter völlig aus. Üblicherweise leiten Sie für die Verwendung des Event Emitters Ihre eigenen Klassen von dieser Basisklasse ab und stellen so die erforderlichen Methoden zur Verfügung.

Eine flexiblere und leistungsfähigere Lösung bietet das Stream-Modul von Node.js. Mit ihm lassen sich Datenströme, die Streams, modellieren. Insgesamt gibt es vier verschiedene Arten von Streams: Readable Streams, aus denen Sie Daten lesen können, Writable Streams, in die Sie schreiben können und Duplex- und Transform Streams, die zwischen einen Readable Stream als Datenquelle und einem Writable Stream als Ziel des Datenstroms gehängt werden können. Somit können Sie, ähnlich wie schon bei den Promises, Ketten von asynchronen Operationen bilden (Listing 3). Die Idee hinter dem Stream-API von Node.js ist, dass Sie sämtliche Glieder der Stream-Kette austauschen können. Das gilt vor allem für die Operationen zwischen dem Anfangs- und Endpunkt. Da die einzelnen Stream-Abschnitte die gleiche Schnittstelle aufweisen, können Sie die Kette beliebig verlängern oder verkürzen.

Das Streams-Modul bietet Ihnen Basis- und Shortcut-Implementierungen, um Ihnen die Arbeit mit den Streams zusätzlich zu erleichtern.

const fs = require('fs');
const read = fs.createReadStream('input.txt');
const write = fs.createWriteStream('output.txt');

const toUpperCase = new require('stream').Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  },
});

read.pipe(toUpperCase).pipe(write);

5. Bibliotheken und Frameworks

Werfen Sie einen Blick auf eine typische Node.js-Applikation, wird Ihnen recht schnell die große Anzahl der installierten NPM-Pakete auffallen. Das ist ein Muster, das sich mittlerweile in der JavaScript-Welt durchgesetzt hat. Existiert für ein Problem eine zufriedenstellende Lösung in Form eines NPM-Pakets, wird dieses installiert und verwendet. Die eingesetzten Bibliotheken weisen unterschiedliche Umfänge und Einsatzgebiete auf – von Struktur-Frameworks, auf denen Sie Ihre Applikation aufbauen, bis hin zu kleinen Hilfsbibliotheken, die Ihnen kleine Routineaufgaben abnehmen. Allen gemein ist, dass Sie sich über den Lebenszyklus Ihrer Applikation hinweg um sie kümmern müssen. Sie sollten in regelmäßigen Abständen prüfen, ob Aktualisierungen veröffentlicht wurden. Mit neuen Versionen Ihrer Abhängigkeiten erhalten Sie nicht nur neue Features und bessere Performance, sondern auch häufig Sicherheitsupdates, die bekannte Probleme beheben und Ihre Applikation so sicherer machen. Neben den Vorteilen bringen externe Bibliotheken auch einige Nachteile mit sich. Ein Major-Update einer Bibliothek kann auch bedeuten, dass sich die Schnittstelle geändert hat. Solche Breaking Changes, also Änderungen, die die Funktionsfähigkeit Ihrer Applikation beeinträchtigen, erfordern Anpassungen am Quellcode Ihrer Applikation. Je nachdem, wie umfangreich die Änderungen sind und wie weit verbreitet die Verwendung der Bibliothek in Ihrer Applikation ist, kann eine solche Anpassung unter Umständen aufwendig werden und muss geplant erfolgen.

Ein wertvolles Werkzeug zum Umgang mit Abhängigkeiten ist das Kommando npm outdated. Es überprüft den Stand der installierten Abhängigkeiten und informiert Sie darüber, ob ein Update ansteht. Aktualisieren Sie die Abhängigkeiten häufig und regelmäßig, sind die erforderlichen Umbaumaßnahmen meist überschaubar und weniger aufwendig als wenn Sie nach einem Jahr oder noch längerer Zeit alles aktualisieren. Ein weiteres Werkzeug, das Ihnen in diesem Zusammenhang Hilfestellung leisten kann, ist das Kommando npm audit. Es überprüft die installierten Abhängigkeiten auf bekannte Sicherheitsprobleme. Die Grundlage für diese Überprüfung ist die Datenbank der Node Security Platform. Wird bei der Überprüfung ein Problem festgestellt, erhalten Sie auch eine Information, ab welcher Version des Pakets die Lücke behoben ist.

6. Datenbankabstraktion

Eine spezielle Kategorie von Paketen, die Sie in Ihrer Applikation nutzen können, sind Datenbankabstraktionen. Node.js macht Ihnen keinerlei Vorgaben, wie und wo Sie die Daten Ihrer Applikation persistieren. Das Dateisystemmodul ist die einzige Möglichkeit, mit der Sie Informationen direkt auf dem Dateisystem speichern können. Was für das Schreiben von Logdateien oder das Auslesen von Konfigurationsdateien noch in Ordnung ist, stößt bei umfangreicheren Schreib- und Leseoperationen schnell an seine Grenzen. Aus diesem Grund sollte Sie die Informationen Ihrer Applikation in einer Datenbank speichern. Welches System Sie hierfür verwenden, liegt ganz bei Ihnen. In Ihre Entscheidung sollten Sie die Menge von Daten und das Format, in dem die Daten vorliegen, einfließen lassen, aber auch wie Ihre Infrastruktur gestaltet ist und welches Datenbanksystem sich am besten integrieren lässt. Schließlich spielt auch noch der Kostenaspekt eine Rolle. Hier können Sie sich entweder für eine kostenfreie Open-Source-Datenbank oder eine kostenpflichtige Datenbank entscheiden. Die gute Nachricht für die Integration in eine Node.js-Applikation ist, dass es für nahezu alle Datenbanken Node.js-Treiber gibt. Diese arbeiten in der Regel immer nach demselben Prinzip: Sie stellen eine Verbindung zur Datenbank her, formulieren Ihre Anfrage und schließen die Verbindung wieder. Die Verarbeitung der Informationen, die die Datenbank zurücksendet, erfolgt im Normalfall asynchron mit Callbacks, Promises oder Streams. Gerade bei relationalen Datenbanken müssen Sie bei dieser Vorgehensweise Ihre Querys selbst schreiben. Dabei sollten Sie unbedingt darauf achten, sämtliche dynamischen Teile korrekt zu escapen, um keine Injection-Attacken zuzulassen. Die meisten Treiber liefern eine escape-Methode mit, die Ihnen diese Aufgabe abnimmt.

Eine elegantere Lösung für den Zugriff auf die Datenbank bieten Abstraktionsbibliotheken. Diese reichen von einfachen Query-Buildern wie Knex bis hin zu vollwertigen ORM-Lösungen wie Sequelize, ORM2 oder Waterline. Diese Bibliotheken unterstützen meist auch unterschiedliche SQL-Dialekte, sodass Sie die Datenbank Ihrer Applikation theoretisch mit wenigen Schritten austauschen können. Der Vorteil einer solchen Abstraktionsschicht ist, dass Sie weniger Brüche in Ihrer Applikation haben. Eine ORM-Bibliothek bietet Ihnen die Möglichkeit, mit JavaScript-Objekten statt mit SQL-Querys zu arbeiten. Die Bibliothek übersetzt die Operationen auf den Objekten, also beispielsweise das Auslesen oder Erzeugen dann in konkrete SQL-Querys. Der Nachteil dieser Lösungen ist, dass die Abstraktion zusätzliche Ressourcen wie Arbeitsspeicher oder CPU-Zeit benötigt.

7. TypeScript

Die Möglichkeit der Strukturierung einer Node.js-Applikation endet jedoch nicht bei der Arbeit mit Objekten, die Objekte in einer Datenbank abstrahieren. Mit TypeScript können Sie eine zusätzliche Struktur und ein Sicherheitselement für Ihren Quellcode während der Entwicklung nutzen. TypeScript ist ein von Microsoft als Open Source entwickeltes Typsystem für JavaScript. Mit den Typangaben von TypeScript wird Ihr Quellcode besser lesbar und auch die Unterstützung von Entwicklungsumgebungen wird im Vergleich zu normalem JavaScript erheblich verbessert. Hier ist vor allem die Überprüfung des Quellcodes schon während der Entwicklung und die Autocompletion zu erwähnen. Beides und noch zahlreiche weitere Features können durch Plug-ins in den gängigen Entwicklungsumgebungen aktiviert werden.

TypeScript erweitert den JavaScript-Quellcode Ihrer Node.js-Applikation um Datentypen, die über die primitiven und composite-Daten von JavaScript hinausgehen. Außerdem haben Sie die Möglichkeit, Variablen und Funktionssignaturen mit Typenangaben zu versehen. TypeScript führt eine statische Typenprüfung Ihres Quellcodes durch und gibt entsprechende Fehlermeldungen aus, falls Verstöße gegen die Vorgaben erkannt werden. TypeScript prüft lediglich die Struktur von Objekten. Diese Vorgehensweise wird als Duck Typing bezeichnet. Ein Objekt besteht die Überprüfung, wenn es dieselbe Struktur aufweist wie eine Instanz des geforderten Typs. Neben zusätzlichen Typen wie beispielsweise Enums und ihren selbst definierten Typen in Form von Klassen können Sie in TypeScript mit Interfaces und Generics arbeiten. Außerdem stellt TypeScript Ihnen ein Modulsystem zur Verfügung, das syntaktisch dem ECMAScript-Modulsystem entspricht.

Node.js ist nicht in der Lage, TypeScript-Code direkt auszuführen. Versuchen Sie das, erhalten Sie in der Regel recht schnell Syntax Errors, da die Typangaben nicht mit der JavaScript-Syntax kompatibel sind. Um Ihre Applikation ausführen zu können, müssen Sie Ihren Quellcode kompilieren. Der Compiler überprüft zunächst den Quellcode auf Fehler und übersetzt dann den TypeScript-Quellcode in JavaScript. Über die Konfiguration, die normalerweise in der tsconfig.json gespeichert wird, können Sie die Arbeit des Compilers beeinflussen und beispielsweise angeben, ob das Resultat mit ECMAScript 5 oder ECMAScript 2017 kompatibel sein soll. Auch für die Übersetzung des Modulsystems haben Sie die Auswahl zwischen mehreren Alternativen wie CommonJS oder ECMAScript. Den übersetzten Quellcode können Sie dann wie gewohnt ausführen. Für die Entwicklung können Sie außerdem ts-node nutzen. Hierbei handelt es sich um eine Kombination aus TypeScript-Compiler und Node.js mit der Sie TypeScript-Dateien direkt ausführen können. Kombiniert mit einem Dateisystem-Watcher wie nodemon können Sie dann den Node.js-Prozess bei Änderungen am Dateisystem automatisch neu starten lassen.

Die meisten NPM-Pakete sind nicht in TypeScript verfasst. Das bedeutet, dass Sie diese zwar in Ihre TypeScript-Applikation einbinden können, jedoch nicht in den Genuss der Unterstützung von TypeScript kommen, wenn Sie auf die Schnittstellen der Pakete zugreifen (Listing 4). Um dieses Problem zu lösen, hat sich die Initiative Definitively Typed zur Aufgabe gemacht, Typdefinitionen für Libraries zu sammeln, die nicht in TypeScript geschrieben sind. Diese Pakete beginnen mit @types/ gefolgt vom Paketnamen. Um beispielsweise die Typdefinitionen für Express zu installieren, führen Sie das Kommando npm install @types/express aus. TypeScript findet die installierten Typdefinitionen automatisch, da der Pfad node_modules/@types durchsucht wird.

import * as express from 'express';

const app = express();

app.get(
  '/',
  (req: express.Request, res: express.Response): void => {
    res.send('Hello Client');
  },
);

app.listen(8080, () => console.log('Server is listening'));

8. Webschnittstellen

Mit Node.js lassen sich alle möglichen Arten von Applikationen umsetzen, am häufigsten wird die Plattform jedoch im Webumfeld eingesetzt und spielt hier die Rolle des Servers. Für die Strukturierung der Schnittstellen nach außen gelten ähnliche Anforderungen wie für die internen Strukturen. Sie sollen leicht verständlich und konsistent sein. Je weniger Dokumentation erforderlich ist, um die Schnittstellen zu verstehen, desto besser. Im Bereich der Webschnittstellen haben sich REST-Schnittstellen durchgesetzt. Mit den verschiedenen HTTP-Modulen bietet Node.js eine solide Grundlage für die Implementierung einer solchen Schnittstelle. Da die Schnittstellen häufig ähnlich aufgebaut sind, sind zahlreiche Frameworks und Bibliotheken entstanden, die Sie bei der Arbeit unterstützen. Eine der populärsten Implementierungen aus diesem Bereich ist Express. Mit seinem Routing-Feature können Sie mit einem überschaubaren Aufwand REST-Schnittstellen erzeugen. Aber auch in der Rolle des Clients funktioniert Node.js problemlos. Sowohl mit Boardmitteln als auch mit externen Bibliotheken wie dem request-Paket können Sie REST-Clients implementieren und damit Schnittstellen abfragen.

Eine der größten Hürden bei der Arbeit mit Webschnittstellen ist eine mangelhafte Dokumentation. Setzen Sie eine Schnittstelle um, können Sie Swagger für die Dokumentation nutzen. Zur Integration in eine Node.js-Applikation existieren verschiedene Pakete. So können Sie swagger-jsdoc nutzen, um öffentliche APIs in Ihrer Applikation mit JSDoc-Kommentaren zu dokumentieren und aus ihnen eine Swagger-Dokumentation zu erzeugen. Diese können Sie mit dem Paket swagger-ui-express über die SwaggerUi als grafische Repräsentation veröffentlichen. Benutzer Ihrer Schnittstellen können sich mit dem Browser mit Ihrem Server verbinden und die Dokumentation inklusive Beispielen einsehen.

Node.js ist nicht nur auf REST-Schnittstellen beschränkt. Auch für SOAP oder GraphQL gibt es Bibliotheken, die Ihnen die meiste Arbeit abnehmen, sodass Sie sich auf die Umsetzung der Applikationslogik konzentrieren können.

Zusammenfassung

Node.js lässt Ihnen viele Freiheiten, wenn es um die Gestaltung von Applikationen geht. Das ist jedoch Fluch und Segen zugleich. Mit dieser Flexibilität benötigen Sie Konventionen und Disziplin im Entwicklungsprozess, um Ihre Applikation auch über einen längeren Zeitraum wartbar und erweiterbar zu halten. In diesem Artikel habe ich Ihnen einige Hilfsmittel und Best Practices vorgestellt, wie Sie dies erreichen können. Die beste Möglichkeit, ein Gefühl für den Aufbau einer Node.js-Applikation zu entwickeln, ist jedoch, es selbst auszuprobieren. Experimentieren Sie mit den verschiedenen Features, die Ihnen Node.js und sein Ökosystem bietet. Entwickeln Sie an einer Applikation, halten Sie diese immer konsistent, wenn es um die verwendeten Architektur- und Designmuster geht, und sorgen Sie dafür, dass immer die aktuellsten Versionen der Abhängigkeiten installiert sind.

Verwandte Themen:

Geschrieben von
Sebastian Springer
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: