Friedrich Siever

13. Januar 2024

Nest.js - Der einfache Einstieg in Datenvalidierung

von: Friedrich Siever | Last Updated: 13.01.24

Einführung

Eine gute Validierung trägt nicht nur zur Datensicherheit bei, sondern verbessert auch die Benutzererfahrung durch klare Rückmeldungen bei fehlerhaften Eingaben. Lass uns gemeinsam eintauchen und entdecken, wie du mit Nest.js die Welt der Datenvalidierung meistern kannst!

Als API-Entwickler liegt unsere Hauptverantwortung darin, sicherzustellen, dass die empfangenen Daten das erwartete Format haben. Stell dir vor, Benutzer geben Telefonnummern in verschiedenen Formaten ein – ohne proaktive Validierung könnten sich schnell erhebliche Unregelmäßigkeiten in unseren Daten ergeben.

Auch wenn unsere Frontend-App bereits eine erste Validierung durchführt (z. B. mit Next.js), ist es entscheidend, dass wir die Daten in unserer API zusätzlich validieren. Dadurch gewährleisten wir, dass die in unserer Datenbank gespeicherten Informationen unseren Vorgaben entsprechen. Erfahre in diesem Beitrag, wie Nest.js und mächtige Tools dir dabei helfen, die Datenvalidierung auf professioneller Basis zu gestalten.

Der Datenfluss

Im gewohnten Ablauf gibt ein Nutzer Daten über seine Webapplikation oder Mobile App ein. Diese werden an unsere API weitergeleitet, und was in unserer API ankommt, nennen wir Raw Data. Diese Rohdaten durchlaufen Validatoren, auch als Pipes bekannt.

In Nest.js ist eine dieser Pipes die Validator Pipe. Im Wesentlichen handelt es sich dabei um einen Satz von vordefinierten Validatoren. Das mag anfangs komplex erscheinen, ist es jedoch nicht. Jeder Validator ist lediglich eine Funktion, die eine einzelne Überprüfung durchführt. Beispiele hierfür sind:

  • Überprüfen, ob die Telefonnummer korrekt ist.
  • Feststellen, ob ein Text die vorgegebene Länge hat.
  • Bestätigen, ob es sich um eine Nummer handelt.

Selbstverständlich ermöglicht Nest.js auch die Erstellung eigener Validatoren. Zum Beispiel könnten wir prüfen, ob die E-Mail-Adresse eines Nutzers, der sich registrieren möchte, noch nicht in unserer Datenbank verwendet wurde.

Nachdem unsere Validatoren überprüft haben, ob die Daten unseren Vorgaben entsprechen und also valide sind, kann unsere App entweder eine Erfolgsantwort an die Webapplikation senden oder eine Meldung darüber, was nicht in Ordnung war.

Insgesamt trägt dies dazu bei, die Nutzererfahrung zu verbessern. Die Nutzer werden entlastet und erhalten Informationen darüber, wenn sie Fehler machen, sowie Anleitungen, wie sie diese beheben können.

Pipes

Pipes sind ein typisches Konzept in der Programmierung von Nest.js. Sie können entweder Transformationen an den Daten vornehmen oder, wie bereits erwähnt, Validierungen durchführen.

Ein Beispiel für die Anwendung von Pipes ist die Überprüfung, ob eine ID im Input tatsächlich eine Nummer ist. Einige Pipes sind direkt in Nest.js eingebaut. Ein Beispiel hierfür ist die ParseIntPipe, die uns die Arbeit erleichtert. Wir können diese direkt in unseren Controller-Methoden verwenden:

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id) {
    console.log(typeof id);
    return await this.repository.findOne(id);
  }

Die Funktionsweise der Pipe lässt sich durch die Verwendung von console.log leicht nachverfolgen. Bei der Kombination mehrerer Pipes können wir diese Klassen einfach durch Kommata getrennt hinzufügen. Dies ermöglicht es uns auch, den Typ der ID nun sicher als Nummer zu deklarieren.

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return await this.repository.findOneBy({ id });
  }

Es ist grundsätzlich ausreichend, den Klassennamen zu verwenden. Wir haben jedoch auch die Möglichkeit, zusätzliche Optionen festzulegen. In diesem Fall müssen wir eine neue Instanz der Klasse mit dem new-Keyword erstellen.

  @Get(':id')
  async findOne(@Param('id', new ParseIntPipe({
    // options here
  })) id) {
    return await this.repository.findOne(id);
  }

“Nachdem wir die grundlegenden Konzepte besprochen haben, richten wir unseren Fokus nun auf die vordefinierten Pipes, die von Nest bereitgestellt werden.

Systemintegrierte Pipes

Nest.js stellt eine Reihe von vordefinierten Pipes zur Verfügung, um die Verarbeitung von Daten zu erleichtern. Hier sind einige der wichtigsten:

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe

Für eine detaillierte Erklärung jeder einzelnen Pipe empfehle ich die ausgezeichneten Validierungsdokumentationen von Nest.js.

Die wahrscheinlich nützlichste Pipe ist die ValidationPipe, die automatisch alle Inputs deiner Anwendung validieren kann. Diese Pipe ist besonders mächtig und kann dazu beitragen, die Integrität und Konsistenz deiner Daten sicherzustellen.

Im nächsten Abschnitt findest du alle Informationen zur Input-Validierung in Nest.js.

Input Validierung in Nest.js

Lass uns lernen, wie wir jeden Input, der in unsere API einströmt, validieren können. Bevor wir jedoch etwas unternehmen, müssen wir die Klassen Validator und Transformer installieren.

npm i class-validator class-transformer

Nach den Installationen schauen wir uns in der Praxis an, wie wir einen Input validieren können. Und das ganz einfach mit Decorator-Argumenten, diesmal jedoch auf dem @Body Decorator.

Als nächstes sollten wir unsere create-exhibitions.dto.ts Datei anschauen.

// src/create-exhibitions.dto.ts
export class CreateExhibitionDto {
  name: string;
  description: string;
  starts: Date;
  ends: Date;
  address: string;
}

Hier können wir nun die umfangreiche “build-in” Library von Validation Decorators verwenden. Das probieren wir gleich mal aus. Die Namen unserer einzelnen Messen sollten zwischen 5 und 55 Zeichen lang sein. In unserer create-exhibitions.dto.ts Datei ergänzen wir wie folgt:

// src/create-exhibitions.dto.ts
import { Length } from "class-validator";

export class CreateExhibitionDto {
  @Length(5, 255)
  name: string;
  description: string;
  starts: Date;
  ends: Date;
  address: string;
}

Das ist wirklich sehr praktisch. Wenn wir nun eine Messe mit einem zu kurzen Namen eingeben, gibt unsere API direkt folgendes sehr sinnvolles JSON zurück:

{
	"message": [
		"name must be longer than or equal to 5 characters"
	],
	"error": "Bad Request",
	"statusCode": 400
}

Fantastisch, oder? Nest.js bietet hier bereits eine nahezu vollständige Lösung out of the box, sodass wir kaum zusätzliche Schritte unternehmen mussten.

Jede Validierungsregel kann spezifische Argumente verarbeiten, sodass du ein höchst flexibles System erstellen kannst. Du kannst dich natürlich durch die Typdefinitionen wühlen und wirst sicher fündig werden. Persönlich schaue ich gerne häufig in der Übersicht der Validierungsgruppen vom class-validator auf GitHub nach. Das geht schneller.

Validation Options

Ich möchte unser kleines Beispiel von oben einen Schritt weiterführen und die Validierungsmeldungen anpassen. Auch das ist kinderleicht in deinem DTO herbeizuführen. Das funktioniert über die sogenannten Validator Optionen.

Ich habe außerdem die Validierungen für die weiteren Felder eingefügt.

// src/create-exhibitions.dto.ts
import { IsDateString, Length } from "class-validator";

export class CreateExhibitionDto {
  @IsString()
  @Length(5, 255, { message: "Die Länge des Namens ist falsch!" })
  name: string;
  @Length(5, 255)
  description: string;
  @IsDateString()
  starts: Date;
  @IsDateString()
  ends: Date;
  @Length(5, 255)
  address: string;
}

Zusätzlich veranschaulicht dieses Beispiel, wie auf einem Feld mehrere Validierungen durchgeführt werden können, indem man sie einfach elegant übereinander stapelt.

Globale Validierung

Du magst dich nun vielleicht fragen, wie das Ganze für unsere Update-Operationen funktioniert. Hier bietet sich eine elegante Alternative zum wiederholten Einsatz der Validierungspipe in jedem Body-Decorator an, so wie wir es zuvor in unserem Controller getan haben.

Die Lösung besteht darin, die Validierung global zu aktivieren. Wie das gemacht wird? Ganz einfach, in unserer main.ts Datei.

// src/main.ts

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // Wichtig: Aktiviere die globale Validierung für jeglichen Applikationsinput
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

Durch diese einfache Anpassung wird jeder Applikationsinput automatisch durch die Validierungspipe geleitet. Wenn ein Input-Typ spezifiziert ist und Validation Decorators verwendet werden, erfolgt die Validierung automatisch.

Bei Verwendung der globalen Validierungspipe in Nest.js wird der Validierungscode vor dem Aufruf der entsprechenden Controller-Methode ausgeführt. Die Validierungspipe wird als Middleware auf Anwendungsebene behandelt, und sie verarbeitet den eingehenden Request, bevor er die Controller-Action erreicht.

Hier ist nochmal der allgemeine Ablauf zur Verdeutlichung:

  1. Der Request trifft in der Anwendung ein.
  2. Die globale Validierungspipe prüft die ankommenden Daten gemäß den Validierungsregeln des jeweiligen DTOs (Data Transfer Object). Hierbei ist es wichtig zu betonen, dass unterschiedliche Endpunkte in der Anwendung verschiedene DTOs verwenden können. Die Validierungspipe wird dabei die spezifischen Regeln des jeweiligen DTOs für den entsprechenden Endpunkt anwenden. Diese Vorgehensweise ermöglicht eine maßgeschneiderte Steuerung der Validierung für verschiedene Aktionen und erlaubt die Nutzung mehrerer DTOs in der Anwendung.
  3. Wenn die Validierung erfolgreich ist, wird die entsprechende Controller-Methode aufgerufen.
  4. Wenn die Validierung fehlschlägt, wird eine entsprechende Fehlerantwort zurückgegeben, und die Controller-Methode wird nicht aufgerufen. Dieser Ansatz ermöglicht eine zentrale und automatische Validierung für alle eingehenden Daten in der gesamten Anwendung.

Bei dieser Vorgehensweise kann unser Controller vollständig unberührt bleiben. Hier ein Beispiel der Globalen Validierung anhand unserer Update Methode.

  @Patch(':id')
  async update(@Param('id') id, @Body() input: UpdateExhibitionDto) {
    const prevData = await this.repository.findOneBy({ id });
    return await this.repository.save({
      ...prevData,
      ...input,
      starts: input.starts ? new Date(input.starts) : prevData.starts,
      ends: input.ends ? new Date(input.ends) : prevData.ends,
    });
  }

Der UpdateExhibitionDto erbt die Validierungsregeln vom CreateExhibitionDto Typ. Um dies zu ermöglichen, kann er folgendermaßen konfiguriert werden:

Weitere Informationen zu den Grundlagen von DTOs findest du in meinem Artikel über NestJS Data Transfer Objekte.

import { PartialType } from "@nestjs/mapped-types";
import { CreateExhibitionDto } from "./create-exhibition.dto";

export class UpdateExhibitionDto extends PartialType(CreateExhibitionDto) {}

Validierung: Gruppen und Optionen

Die globale Einstellung für die Validierung, die wir im vorherigen Abschnitt vorgenommen haben, ist praktisch und einfach. Sie leitet jedoch alle Validierungseinstellungen für jede Route der Anwendung weiter. Das bedeutet, dass die globalen Einstellungen immer und überall verwendet werden.

Es kann jedoch Situationen geben, in denen du spezifischere Validierungsoptionen für bestimmte Routen festlegen möchtest. Nest.js bietet hierfür die Möglichkeit, Validierungsgruppen und optionale Einstellungen auf Routenebene zu definieren.

Durch die Verwendung von Validierungsgruppen kannst du die Validierung für bestimmte Endpunkte anpassen, während die globalen Einstellungen weiterhin für andere Teile der Anwendung gelten. Dies ermöglicht eine feinere Steuerung und Anpassung der Validierung je nach Anforderungen der einzelnen Routen.

In den kommenden Abschnitten werden wir uns genauer damit befassen, wie du Validierungsgruppen und optionale Einstellungen auf Routenebene festlegen kannst. Dies bietet Flexibilität und ermöglicht es, die Datenvalidierung in Nest.js genau auf die Bedürfnisse deiner Anwendung abzustimmen.

Validierung pro Route

Obwohl die zuvor eingeführte globale Validierungseinstellung eine praktische und einfache Anwendung auf globaler Ebene ermöglicht, gibt es Situationen, in denen spezifischere Validierungskonfigurationen für bestimmte Routen erforderlich sind. Dies führt uns zur Diskussion über Validierungsgruppen.

Validierungsgruppen sind ein leicht verständliches Konzept in Nest.js und funktionieren ähnlich wie Tags oder Labels. Innerhalb deiner Input-Klassen kannst du festlegen, dass bestimmte Validierungsregeln nur dann angewendet werden sollen, wenn eine spezielle Validierungsgruppe aktiv ist, zum Beispiel “Create” oder “Update”.

Durch die Konfiguration der Validierung pro Route erhältst du die Flexibilität, Validierungsregeln basierend auf der spezifischen Route oder Aktion anzupassen. Dies ermöglicht eine präzisere Steuerung der Validierung, insbesondere wenn verschiedene Teile deiner Anwendung unterschiedliche Validierungsanforderungen haben. Lass uns weiter erkunden, wie diese Flexibilität in der Praxis umgesetzt werden kann.

Kontext

Grundsätzlich geht es darum, unseren Input abhängig vom Kontext auf unterschiedliche Weise zu validieren. Werfen wir dazu einen erneuten Blick auf unsere Data Transfer Objects (DTO).

Lassen wir für einen Moment außer Acht, ob die folgenden Regelungen logischen Sinn ergeben. Wir könnten Folgendes definieren:

//
import { IsDateString, IsString, Length } from "class-validator";

export class CreateExhibitionDto {
  //src/create-exhibition.dto.ts
  @Length(5, 255, { groups: ["create"] })
  @Length(5, 20, { groups: ["update"] })
  address: string;
}

Um diese Regelungen wirksam werden zu lassen, müssen wir den Controller der entsprechenden Routen anpassen.

// ...

@Controller("/exhibitions")
export class ExhibitionsController {
  // ...

  @Post()
  async create(
    @Body(new ValidationPipe({ groups: ["create"] }))
    input: CreateExhibitionDto,
  ) {
    return await this.repository.save({
      ...input,
      starts: new Date(input.starts),
      ends: new Date(input.ends),
    });
  }

  @Patch(":id")
  async update(
    @Param("id") id,
    @Body(new ValidationPipe({ groups: ["update"] }))
    input: UpdateExhibitionDto,
  ) {
    const prevData = await this.repository.findOneBy({ id });
    return await this.repository.save({
      ...prevData,
      ...input,
      starts: input.starts ? new Date(input.starts) : prevData.starts,
      ends: input.ends ? new Date(input.ends) : prevData.ends,
    });
  }
}

Im Parameter des Body Decorators erstellen wir eine neue Validation Pipe, der wir wiederum die Validierungsgruppe(n) mitgeben. Damit das funktioniert, müssen wir auch die globale Validierungspipe in unserer main.ts ausschalten.

Ob du eine Pipe global oder lokal verwendest, liegt an dir als API-Entwickler. Wie bei allem im Leben gibt es Vor- und Nachteile.

Globale Verwendung:

Pros:

  • Einfache Anwendung: Die globale Validierungspipe wird einmalig konfiguriert und gilt automatisch für alle Endpunkte der API.
  • Konsistenz: Die gleichen Validierungsregeln gelten für alle Routen, was zu einer konsistenten Datenvalidierung führt.
  • Globale Anpassungen: Änderungen an den Validierungsregeln können zentral an einer Stelle vorgenommen werden, was die Wartung erleichtert.

Cons:

  • Mangelnde Flexibilität: Es kann schwierig sein, spezifische Validierungsregeln für bestimmte Routen oder Aktionen einzuführen, da die globale Einstellung für alle gilt.
  • Overhead: Die globale Validierung kann Overhead verursachen, da sie für alle Anfragen aktiv ist, unabhängig davon, ob alle Regeln benötigt werden.

Lokale Verwendung:

Pros:

  • Granulare Steuerung: Entwickler haben die Möglichkeit, Validierungspipes für bestimmte Controller-Methoden oder Routen individuell zu konfigurieren.
  • Flexibilität: Unterschiedliche Validierungsregeln können für verschiedene Teile der Anwendung angewendet werden, was zu höherer Flexibilität führt.
  • Klarere Intention: Die Validierungsregeln sind direkt an der Stelle definiert, an der sie benötigt werden, was die Codeverständlichkeit erhöht.

Cons:

  • Mehr Konfiguration: Lokale Validierung erfordert möglicherweise mehr Konfiguration für jede Route, was die Entwicklungszeit leicht erhöhen kann.
  • Potenzielle Redundanz: Bei ähnlichen Validierungsanforderungen für mehrere Routen kann es zu Redundanzen in der Codekonfiguration kommen.

Die Entscheidung, ob globale oder lokale Validierungspipes verwendet werden sollen, hängt von den Anforderungen der Anwendung ab. In der Regel ist eine sorgfältige Abwägung zwischen Konsistenz und Flexibilität erforderlich.

Fazit

Dieser Artikel bietet nicht nur einen soliden Einstieg in die Welt der Datenvalidierung mit Nest.js, sondern unterstreicht auch die essentielle Bedeutung dieser Praxis für die Integrität und Konsistenz von Anwendungsdaten. Die leistungsfähige ValidationPipe von Nest.js, in Kombination mit eingebauten Pipes und Tools wie class-validator und class-transformer, ermöglicht eine effektive und zentrale Validierung von Eingabedaten in der gesamten Anwendung.

Die Bedeutung einer guten Datenvalidierung erstreckt sich jedoch weit über die Sicherheit der Daten hinaus. Eine effiziente Validierung trägt maßgeblich dazu bei, die Benutzererfahrung zu verbessern. Klare Rückmeldungen bei fehlerhaften Eingaben ermöglichen es den Nutzern, Fehler schnell zu erkennen und zu korrigieren. Diese präzisen Fehlermeldungen fördern nicht nur die Benutzerfreundlichkeit, sondern auch die Effizienz und Zufriedenheit der Anwender.

Nutze die vielfältigen Möglichkeiten von Nest.js, um eine umfassende und benutzerfreundliche Validierung in deinen Projekten zu implementieren. Mit dem klaren Verständnis der Bedeutung der Datenvalidierung und den leistungsstarken Tools von Nest.js stehst du vor einer erfolgreichen Umsetzung in deinen Anwendungen. Viel Freude beim Erkunden und Implementieren!

0
0 Bewertungen

Jetzt selbst bewerten