zurück zum Artikel

Eine Einführung in Nest.js

Sebastian Springer
Eine Einführung in Nest.js

(Bild: Blackboard/Shutterstock.com)

Für Webapplikationen mit Node.js ist das Framework Express der Platzhirsch schlechthin. Eine willkommene Abwechslung bietet Nest.js, das moderne Entwicklungsmuster in die Serverwelt bringt.

Node.js erfreut sich in der Webentwicklung seit mehreren Jahren stetig wachsender Beliebtheit. Auf Basis der JavaScript-Plattform entstehen zahlreiche Entwicklungswerkzeuge sowie Backends für Webapplikationen. Jedoch gibt es einen großen Kritikpunkt, wenn es um den Einsatz von Web-Application-Frameworks wie Express geht: die fehlende Architektur.

Diese Frameworks bieten zwar zahlreiche Features und ein Plug-in-System, mit dem Entwickler zusätzliche Erweiterungen laden können, um die Arbeit zu vereinfachen. Allerdings machen Express und Co. keine Angaben, wie die einzelnen Bestandteile einer Applikation zusammenwirken oder wie das Framework sie gruppiert. Das versucht ein relativ junges Projekt zu lösen. Die Rede ist von Nest, einem Framework für Node.js, um Backends für Webapplikationen zu bauen. Das Ziel von Nest ist eine Steigerung der Entwicklungseffizienz. Hierfür bedient sich das Framework sowohl bei etablierten als auch bei neuen Features der JavaScript-Welt.

Mehr Infos

Nest.js auf der enterJS

Auf der von heise Developer, iX und dpunkt.verlag veranstalteten Konferenz für Enterprise-JavaScript enterJS [1] ist Sebastian Springer unter anderem mit einem Vortrag zu Nest.js im Programm zu sehen [2].

Die Entwickler von Nest haben die Stärken verschiedener JavaScript-Frameworks und Bibliotheken zu einem neuen zeitgemäßen Framework kombiniert. Entwickler, die mit dem Frontend-Framework Angular gearbeitet haben, erkennen zahlreiche Muster wieder – die Nest-Macher nennen das Framework eine große Inspiration. Doch wie lassen sich Architektur- und Entwurfsmuster aus dem Frontend auf das Backend übertragen?

Die Grundlage von Nest bildet nicht JavaScript, sondern das Superset TypeScript. Das gesamte Framework ist in TypeScript geschrieben und befindet sich damit in guter Gesellschaft. Sowohl Angular als auch die für 2019 geplante Version 3 des Frameworks Vue.js sind in TypeScript entwickelt. Auch Entwickler bei Facebook liebäugeln zunehmend mit TypeScript: Das zeigt die Unterstützung von TypeScript bei Create-React-App oder die Tatsache, dass man das Testframework Jest nach TypeScript portiert hat.

Nest verfolgt darüber hinaus einen modularen Ansatz. Er ist nicht nur beim Aufsetzen einer Applikation bemerkbar, wenn Entwickler sie in lose gekoppelte Module unterteilen können, sondern auch im Kern des Frameworks, wo Anwender das zugrunde liegende Applikations-Framework wenn nötig austauschen können.

Beim grundsätzlichen Setup folgt Nest bewährten Pfaden, modernisiert sie allerdings teilweise erheblich. So bleibt das von Express gewohnte Routing erhalten. Nest konfiguriert die Routen jedoch nicht im Quellcode, sondern annotiert sie über TypeScript-Decorator. Überhaupt nutzt Nest die aus Angular bekannten Dekoratoren recht intensiv. Die Funktionen hinter den einzelnen Routen sind in Controller-Methoden organisiert. Sie sollen sich um die Extraktion von Informationen aus der eingehenden Anfrage und die Erzeugung einer Antwort an den Client kümmern.

Die Businesslogik der Applikation befindet sich in separaten Service-Klassen, die Entwickler über Dependency Injection laden können. Den Zugriff auf Datenquellen wie Datenbanken oder Webservices können Nutzer, ähnlich wie die Businesslogik, über Services kapseln. Auch die Integration einer Datenbankabstraktionsschicht ist in nur wenigen Schritten möglich. Damit ist das Ziel, die Standardelemente eines Web-Backends in kurzer Zeit lauffähig zu haben, mit Nest in greifbare Nähe gerückt. Sie bilden die Basis für eine modulare Applikation mit einem Fokus auf guter Wartbarkeit und Erweiterbarkeit. Um den Boilerplate-Code, der für eine Applikation von Hand geschrieben werden muss, auf ein Minimum zu reduzieren, bietet Nest eine Kommandozeilenschnittstelle, die die Applikation über ihren gesamten Lebenszyklus begleitet.

Nest steht für eine hohe Entwicklungsgeschwindigkeit. Das macht sich zu Beginn der Entwicklung einer Applikation bemerkbar. Während Entwickler bei anderen Node.js-Frameworks die grundlegende Struktur einer Applikation noch von Hand aufbauen müssen oder auf vorgefertigte Seed-Projekte zurückgreifen, geht Nest einen anderen Weg, der im Frontend schon länger etabliert ist. Nest setzt zur Initialisierung und während der Entwicklung der Applikation auf eine Kommandozeilenschnittstelle (Command-line Interface, CLI). Einer der ersten Vertreter, die die Strategie verfolgt haben, war das Frontend-Framework Ember. Angular, React und Vue haben mittlerweile nachgezogen und bieten ebenfalls solche Werkzeuge.

Die Installation des Nest-CLI erfolgt über einen Paketmanager – entweder Yarn oder npm. Als Kommandozeilenwerkzeug installiert man das Nest-CLI in der Regel global, damit der nest-Befehl systemweit zur Verfügung steht. Mit dem üblichen Befehl npm install -g @nestjs/cli lässt sich das Werkzeug installieren. Wie der Name des Pakets vermuten lässt, nutzt Nest NPM Scopes, also den Präfix @nestjs, um einen gemeinsamen Namespace für alle Kernpakete des Frameworks zu bieten. Die Entwicklung des Nest-Kerns läuft in einem Monorepo auf GitHub ab [3]. Daraus gehen beispielsweise die Pakete @nestjs/core oder @nestjs/common hervor. Für zahlreiche Erweiterungen und Werkzeuge, wie das CLI, existieren weitere Repositorys innerhalb der Nestjs-Organisation.

Der nest-Befehl weist folgende Struktur auf: nest [Kommando] [...Optionen]. Die wichtigsten Befehle sind new zum Erstellen einer neuen Applikation und generate zum Erzeugen neuer Strukturen in einem bestehenden Projekt. Für beide existieren mit n beziehungsweise g auch Abkürzungen. Mit den zusätzlichen Optionen können Entwickler das Verhalten der Befehle spezifizieren. So akzeptiert der new-Befehl den Namen der zu erzeugenden Applikation. generate erhält die Bezeichnung sowie den Namen der zu erstellenden Struktur. Das sieht bei einem Controller mit dem Namen "user" wie folgt aus:

nest generate controller user 

oder in Kurzschreibweise

nest g co user

Den ersten Schritt in einer Nest-Applikation bildet nest new myApp, wobei "myApp" der Name der Applikation und gleichzeitig der Name des Wurzelverzeichnisses der Applikation ist. Im Verzeichnis erzeugt Nest die initiale Verzeichnis- und Dateistruktur der Applikation. Während des Erstellungsprozesses der neuen Applikation fragt das Nest-CLI den Entwickler, welcher Paketmanager zum Einsatz kommen soll, Yarn oder npm. Nest verwendet ihn zur Installation aller erforderlichen Abhängigkeiten.

Nach Abschluss des Prozesses liegt eine voll funktionsfähige Webapplikation vor, die Entwickler mit npm start starten können. Alternativ dazu ist es möglich, die Applikation im Entwicklungsbetrieb auszuführen, und zwar mit dem Kommando npm run start:dev. Dabei findet das Werkzeug nodemon Verwendung, um die Applikation bei Änderungen am Quellcode automatisch neu zu starten. Das eignet sich für den Produktivbetrieb nicht, da ein Neustart zum Verlust des aktuellen Applikationszustandes und der Terminierung aller offenen Kommunikationsverbindungen zu den aktiven Clients führt. Stattdessen führt man in diesem Fall die Applikation mit dem Befehl npm run start:prod aus. Das sorgt dafür, dass die Applikation in JavaScript übersetzt und anschließend gestartet wird.

Nach der Initialisierung lassen sich die Zusammenhänge einer Nest-Applikation am besten anhand eines einfachen Beispiels erläutern. Dazu dient in diesem Artikel die Umsetzung einer Schnittstelle zum Anlegen eines neuen Benutzerdatensatzes. Die Funktionen in einer Nest-Applikation sind üblicherweise in Modulen organisiert.

Bei einem Modul handelt es sich um eine Sammlung von thematisch zusammengehörigen Elementen. Um es zu erzeugen, können Entwickler den Befehl nest g mo user nutzen. Das Modul befindet sich im User-Verzeichnis in der Datei user.module.ts. Im Kern handelt es sich um eine TypeScript-Klasse, die mit einem Decorator versehen ist. Die Decorator fügen zusätzliche Informationen zur Klasse hinzu. Über den Decorator können Nutzer insgesamt vier Informationen übergeben:

Module sind ein Mittel zur Strukturierung der Applikation, sie enthalten nur wenig bis keine Applikationslogik. Der wirkliche Einstieg in die Applikation sind die Controller.

Im User-Modul legen Entwickler nach der Initialisierung der Applikation den User-Controller mit[ i]nest g co user[/i] an. Der Befehl erzeugt im user-Verzeichnis die beiden Dateien user.controller.ts und user.controller.spec.ts. Die zweite Datei enthält einen vorgefertigten Test für den Controller, der auf dem Test-Framework Jest basiert.

Den Controller registriert man im User-Modul. Das macht ihn aktiv und bereit für den Einsatz. Im Controller befindet sich der @Controller-Decorator, der die Route als Zeichenkette erhält. Die Controller-Klasse ist nur ein Teil der Struktur, die zum Erzeugen des neuen Datensatzes erforderlich ist. Die Methoden der Klasse kommen zum Einsatz, um mit den eingehenden Anfragen umzugehen. Dazu dienen wiederum Decorator.

Der Name der Methode spielt bei den Funktionen keine Rolle, man kann sie beliebig benennen. Man sollte ihn allerdings so wählen, dass er Rückschlüsse auf den Zweck der Methode zulässt. Der wirklich relevante Teil ist der verwendete Decorator. Zur Erzeugung eines neuen Datensatzes dient der @Post-Decorator. Nest unterstützt ebenfalls weitere Decorator, die den Namen von HTTP-Methoden wie Get, Put oder Deletetragen. Ihnen können Anwender einen weiteren Pfad als Zeichenketten übergeben. Darin können auch, durch einen Doppelpunkt eingeleitet, Variablen enthalten sein. Innerhalb der Controller-Methode kann man dann auf den Teil des URL-Pfads zugreifen.

Zurück zum Anlegen des Benutzeraccounts: Bei einer HTTP-Post-Anfrage werden die Informationen über den Datensatz üblicherweise im Body der Anfrage übergeben. Um darauf zugreifen zu können, kommt der @Body-Decorator in der Parameterliste der Methode zum Einsatz. Er erweitert den nachfolgenden Parameter dahingehend, dass er mit den Informationen aus dem Body der Anfrage bestückt wird. Da Nest auf TypeScript basiert, können Entwickler außerdem angegeben, welchen Objekttyp der Body der Nachricht haben soll.

Die Controller-Methode sollte nicht über allzu viel Funktionalität verfügen. Deshalb empfiehlt es sich, die weitere Logik, die zum Anlegen des Benutzers nötig ist, in einen Service auszulagern. Diesen bindet die Dependency Injection von Nest in den Controller ein. Geht man davon aus, dass es einen UserService gibt, der über eine addUser-Methode verfügt, führt das zu folgendem Quellcode für den Controller.

import { Controller, Body, Post } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
constructor(private userService: UserService) {}

@Post()
addQuiz(@Body() body: User) {
return this.userService.addUser(body);
}
}

Die Dependency Injection ist als Constructor-Injection implementiert. Zunächst gibt man den Zugriffsmodifikator für die Eigenschaft an, gefolgt vom Namen und schließlich dem Typ der Eigenschaft. Im Falle eines zu injizierenden Services ist das die Klasse des Services. Nest prüft danach, ob es bereits eine Instanz des Services gibt, legt bei Bedarf eine neue an und stellt anschließend die Instanz über die Eigenschaft des Controllers zur Verfügung.

In der addUser-Methode des Controllers führt Nest die addUser-Methode des Services aus. Das return-Statement sorgt dafür, dass der Rückgabewert des Services die Antwort an den Client darstellt. Eine Besonderheit ist die Unterstützung sowohl synchroner als auch asynchroner Rückmeldungen. Liefert der Service beispielsweise ein Promise-Objekt zurück, wartet Nest mit der Antwort an den Client, bis es gelöst ("resolved") oder abgelehnt ("rejected") ist. Der Service kümmert sich dann um die eigentlichen Aufgaben im Zusammenhang mit der Erzeugung des Datensatzes.

Entwickler können den User-Service mit dem Befehl nest g s user im User-Modul erzeugen. Der Service ist automatisch im Modul registriert, sodass die Dependency Injection direkt funktioniert. Er kümmert sich um Berechnungen, Validierung und die Persistierung der Information. Die meisten der Aufgaben landen wiederum bei speziellen Elemente des Frameworks, sodass der Service sie lediglich verteilt.

Ein klassisches Beispiel hierfür ist die Kommunikation mit einer Datenbank. Statt die Verbindung manuell aufzubauen und Datenbankanfragen direkt zu formulieren, besteht die Möglichkeit, auf ein ORM zur Datenbankabstraktion zurückzugreifen. Nest empfiehlt, auf TypeORM zurückzugreifen. Im einfachsten Fall führt das zu folgendem Quellcode:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../user.entity';
import { Repository } from 'typeorm';

@Injectable()
export class QuizService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}

async addUser(user: User): Promise<User> {
const newUser = await this.userRepository.save(user);
return newUser;
}
}

Der Konstruktor injiziert das userRepository, das die Verbindung zur Datenbank übernimmt und im Service eine Objektschnittstelle zur Datenbank bietet. Die addUser-Methode greift nur noch auf die save-Methode des Repositorys zu, die für das Speichern der Benutzerdaten sorgt. Eine Besonderheit an der addUser-Methode ist, dass sie als asynchrone Methode implementiert ist. Sie gibt also in jedem Fall ein Promise-Objekt zurück und erlaubt gleichzeitig die Verwendung des await-Schlüsselworts, um auf asynchrone Operationen wie den Speichervorgang, der ebenfalls ein Promise-Objekt zurückgibt, zu warten. Da auch der Controller die Verwendung von Promises unterstützt, lässt sich der Quellcode kompakt halten.

Für die Datenbankanbindung müssen Entwickler zunächst TypeORM, der Datenbanktreiber, und das Nest-Modul installieren. Das Beispiel nutzt SQLite als Datenbank. TypeORM unterstützt außerdem noch zahlreiche weitere SQL-Datenbanken und MongoDB. Die Installation der benötigten Pakete erfolgt mit npm install @nestjs/typeorm typeorm sqlite3. Danach müssen Anwender die Datenbankverbindung konfigurieren. Das kann entweder direkt im zentralen App-Modul erfolgen oder in einer separaten ormconfig.json-Datei. Die zweite Variante ist die elegantere, da sie dafür sorgt, dass das Appmodul nicht unnötig anwächst. In den Imports des Appmoduls trägt man das TypeOrmModule ein, das für die Datenbankverbindung sorgt.

Nach den initialen Setup-Aufgaben erzeugt man eine Entity-Klasse für die User-Tabelle. Sie gibt die Struktur der Objekte vor und enthält Informationen über die Datentypen der jeweiligen Spalten. Mit der Konfiguration und den entsprechenden [i]Entity[i]-Klassen können Nutzer das zugehörige Repository für den Datenbankzugriff verwenden. Der Quellcode einer Entity-Klasse setzt sich wie folgt zusammen:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column('text')
firstname: string;

@Column('text')
lastname: string;

@Column('text')
birthday: string;

@Column('text')
address: string;

@Column('text')
email: string;
}

Mit diesem Stand der Applikation ist es möglich, über Werkzeuge wie cURL oder Postman auf die Schnittstelle zuzugreifen und neue Datensätze anzulegen. Die Applikation können Entwickler anschließend um weitere Funktionen wie eine Authentifizierung erweitern.

Wie das vorangegangene Beispiel zeigt, ist die Entwicklungsgeschwindigkeit für einfache Webschnittstellen relativ hoch. Neben solchen Problemstellungen deckt Nest in ähnlicher Weise zahlreiche weitere Anwendungsfälle ab. Das ermöglicht der modulare Aufbau des Frameworks. Die Integration von Logging ist in nur wenigen Zeilen möglich, auch verschiedene Authentifizierungsstrategien lassen sich problemlos einbinden.

Nest sieht aber auch weiterführende Themen wie die Unterstützung von Microservices, die Kommunikation über Websockets oder die Erzeugung von GraphQL-Schnittstellen vor. Für solche Aufgaben sind zusätzliche Schnittstellen und Module bereits vorbereitet und Entwickler müssen sie nur noch einbinden, konfigurieren und gegebenenfalls mit applikationsspezifischer Logik versehen.

Nachdem über viele Jahre Express das dominierende Framework für das Entwickeln von Webapplikationen in Node.js war und sich wenig bis nichts Neues getan hat, ist Nest eine willkommene Abwechslung. Es bringt moderne Entwicklungsmuster in die Serverwelt von Node.js. Die Community, die sich um Nest gebildet hat, wächst von Tag zu Tag und bietet zahlreiche Lösungsansätze für alltägliche Probleme, die sich auch gut in bestehende Applikationen integrieren lassen. Geht es um die Implementierung einer Webschnittstelle auf Basis von Node.js, sollte Nest auf jeden Fall auf der Liste der Kandidaten für eine Umsetzung stehen.

Sebastian Springer
arbeitet als JavaScript-Entwickler bei der Maiborn Wolff GmbH und ist Dozent für JavaScript, Sprecher auf zahlreichen Konferenzen und Autor diverser Fachartikel zum Thema JavaScript.
(bbo [4])


URL dieses Artikels:
https://www.heise.de/-4418690

Links in diesem Artikel:
[1] https://www.enterjs.de/
[2] https://www.enterjs.de/programm?source=12
[3] https://github.com/nestjs/nest
[4] mailto:bbo@ix.de