Mit Node.js hält JavaScript immer mehr Einzug auf der Serverseite. Den großen Sprung in die Enterprise-Welt hat Node.js aber noch nicht geschafft. Das Framework Nest.js tritt an, dies zu ändern.

Das Hauptproblem, warum Node.js noch nicht abgehoben ist, sehen die Entwickler von Nest.js in der Architektur von Node.js-Anwendungen. Mit ihrem Framework wollen sie das Entwickeln von testbaren, skalierbaren und wartbaren Node.js-Anwendungen ermöglichen. Ob das gelungen ist, und um einen Überblick über Nest.js zu geben, wird das Framework im Folgenden näher betrachtet.

Die Entwickler schreiben selbst, dass sie sich von Angular inspirieren ließen. Das erkennt man schon daran, dass es out-of-the-box mit TypeScript kommt. Im Hintergrund verwendet Nest.js das bekannte Framework Express. Außerdem ist Nest.js kompatibel mit vielen Bibliotheken wie beispielsweise Fastify oder bereits verfügbaren Third-Party Plugins.

Ein neues Projekt starten

Natürlich kommt Nest.js mit einer Command Line Interface, die Nest CLI. Um die CLI global zu installieren, benutzen wir folgenden Command:

$ npm i -g @nestjs/cli

Zur Demonstration schreiben wir eine Anwendung, die Daten von zwei Ländern vergleicht. Damit wir nicht komplett von “Scratch” beginnen müssen, klonen wir das Starter-Projekt vom Nest.js Repository:

git clone https://github.com/nestjs/typescript-starter.git world-comparison
cd world-comparison
npm install

Die Anwendung kann man mit dem bekannten Node-Command starten:

$ npm run start

Wir wollen aber nodemon verwenden, was bereits im Starter-projekt enthalten ist. Nodemon trackt Codeänderungen und startet den Server automatisch neu.

$ nodemon

Der Server startet auf dem Port 3000. Falls der Port bereits verwendet wird, kann der Port in der Datei src/main.ts geändert werden.

Der Controller

Das Starter-Projekt enthält bereits auch einen ersten Controller namens AppController. Ein Blick in den Controller lässt schon erahnen, was er macht:

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

Der Controller empfängt GET-Requests und führt die Methode getHello() aus. Die Methode wiederum holt sich vom AppService einen String und gibt ihn zurück. Rufen wir also localhost:3000 auf, sehen wir im Browser den String “Hello World!”.

Schauen wir uns den AppController genauer an. Zunächst ist es eine simple JavaScript-Klasse, angereichert mit einem @Controller() Decorator. Im Decorator kann eine Route angegeben werden, z.B. @Controller('world'). Dadurch ändert sich der Pfad zu localhost:3000/world.

@Get() markiert eine Methode, die bei einem GET-Request ausgeführt wird. Gibt die Methode ein JavaScript Objekt oder Array zurück, wird der Return-Value automatisch serialisiert.

Controller und Service anlegen

Mithilfe der Nest CLI lassen sich Controller schnell erstellen:

nest g controller controllers/density

Der neue Controller soll etwas mehr Funktionalität erhalten. Er soll zwei Ländernamen als Queryparameter empfangen und zurückgeben, welches Land dichter besiedelt ist. Die Daten holen wir über die offene API von restcountries.eu.

Der Controller soll dazu über einen Service mit der API kommunizieren. Also erstellen wir einen Service mit der CLI:

nest g service services/country

Der neue Controller DensityController und der neue Service CountryService wurden automatisch im AppModule importiert:

@Module({
  imports: [],
  controllers: [AppController, DensityController],
  providers: [AppService, CountryService],
})
export class AppModule {
}

Die Projektstruktur sieht nun wie folgt aus:

Projektstruktur

HTTP Requests mit Nest.js

Die Länder-Daten werden mit HTTP Requests von restcountries.eu abgerufen. Hierfür wird der HTTP Client Axios in Nest.js verwendet. HttpService ist Teil von @nestjs/common und lässt sie von dort im CountryService importieren:

import { HttpService } from '@nestjs/common';

Außerdem muss HttpModule im AppModule-Kontext verfügbar sein. Daher importieren wir HttpModule in AppModule und fügen es den Imports hinzu.

import { HttpModule, Module } from '@nestjs/common';
// ...

@Module({
  imports: [
    HttpModule,
  ],
  // ...
})
export class AppModule {
}

Anschließend fügen wir der noch leeren CountryService-Klasse einen Konstruktor und eine Methode hinzu:

@Injectable()
export class CountryService {
  constructor(private readonly httpService: HttpService) { }

  getCountry(country: string): Observable<AxiosResponse<CountryDto>> {
    return this.httpService.get(`https://restcountries.eu/rest/v2/name/${country}`);
  }
}

Betrachten wir den Rückgabewert von getCountry() genauer. Die get-Methode von HttpService gibt ein Observable zurück. Dies ist ein generischer Typ, mit dem ein Typargument angegeben werden muss. Hierfür stellt Axios die Interface AxiosResponse bereit. Das Typargument von AxiosResponse ist CountryDto. Dieses Data Transfer Object müssen wir noch anlegen:

 $ nest g class services/country/CountryDto

Mit der Angabe des Pfades wird das DTO in dem Pfad erstellt, wo CountryService liegt.

Die API von restcountries.eu gibt sehr viele Informationen eines Landes zurück. Für unseren Zweck brauchen wir nur “name”, “population” und “area”. Deshalb definieren wir das DTO wie folgt:

export class CountryDto {
  readonly name: string;
  readonly population: number;
  readonly area: number;
}

Das DTO muss noch im Service importiert werden:

import { CountryDto } from './country-dto';

Der Service ist nun bereit, um im Controller verwendet zu werden. Hier wird die Logik implementiert. Die Schnittstelle soll die Bevölkerungsdichte zweier Länder vergleichen. Dazu ruft der CountryService die Informationen beider Länder mit jeweils einem Request ab. Wir warten auf die Antwort der restcountries-API, indem wir async/await verwenden. getCountry() gibt ein Observable zurück, welches mit toPromise() zu einem Promise umgewandelt wird. Sobald die Daten beider Länder verfügbar sind, vergleichen wir die Bevölkerungsdichte und geben ein Promise zurück.

@Get()
  async getDensestCountry(
    @Query('country1') country1: string,
    @Query('country2') country2: string,
  ): Promise<string> {
    const res1 = await this.countryService.getCountry(country1).toPromise();
    const res2 = await this.countryService.getCountry(country2).toPromise();
    const c1: CountryDto = res1.data[0];
    const c2: CountryDto = res2.data[0];

    if (c1.population / c1.area > c2.population / c2.area) {
      return `${c1.name} is denser than ${c2.name}.`;
    } else {
      return `${c2.name} is denser than ${c1.name}.`;
    }
  }

Wir haben nun eine Schnittstelle, die auf der Route /density abrufbar ist und zwei Queryparameter conutry1 und country2 erwartet. Das wollen wir mit Postman testen:

Testen der Schnittstelle in Postman

Die Schnittstelle gibt erfolgreich einen String zurück!

Nächste Schritte

Die Dokumentation von Nest.js ist übersichtlich gestaltet und bietet Informationen von Authentifizierung über Middleware bis Datenbankanbindung. Wer das Beispiel-Projekt aus diesem Blogartikel lokal ausführen möchte, findet den Code auf GitHub.