Nest.js ValidationPipe: Der klassische Weg
TypeScript bietet im Backend-Bereich einen entscheidenden Vorteil: Typen und Interfaces lassen sich problemlos zwischen Frontend und Backend austauschen. In einem Nx Workspace gelingt das besonders einfach über gemeinsame Libraries. Damit sind viele Fehler auf Typebene bereits ausgeschlossen. Allerdings verschwindet die Typprüfung von TypeScript zur Laufzeit vollständig, und REST-Schnittstellen kommunizieren in der Regel über untypisiertes JSON. Deshalb ist eine zusätzliche Validierung der Daten im Backend unerlässlich – sowohl für die Sicherheit der Schnittstelle als auch der gesamten Anwendung. Darüber hinaus können durch die Validierung bereits fachlich unsinnige Daten abgefangen oder zurückgewiesen werden.
Dieser Beitrag stellt einen alternativen Ansatz zur validierung von Daten mit der Bibliothek zod vor, der sich von Nest.js' eingebauter ValidationPipe unterscheidet.
Nest.js bringt mit der ValidationPipe bereits eine eingebaute Lösung mit. Diese basiert auf den npm-Paketen class-validator und class-transformer und folgt einem objektorientierten, deklarativen Muster. Zuerst wird die Datenstruktur als Klasse modelliert, anschließend ergänzen Decorators die Validierungsregeln als Metadaten.
Ein Beispiel dafür sieht so aus:
import { IsBoolean, IsDateString, IsString, MaxLength, MinLength } from 'class-validator';
export class FlightDto {
@IsString()
@MinLength(5)
@MaxLength(100)
from: string;
@IsString()
@MinLength(5)
@MaxLength(100)
to: string;
@IsDateString()
time: string;
@IsBoolean()
delayed: boolean;
}
Im Nest.js Controller kommt die Validierung dann folgendermaßen zum Einsatz:
import { Body, Controller, Get, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { FlightService } from './flight.service';
import { FlightDto } from '@angular-nestjs-zod/shared/util/api-models';
@Controller('flights')
export class FlightController {
constructor(private readonly flightService: FlightService) {}
@Get()
getFlights(): FlightDto[] {
return this.flightService.getAllFlights();
}
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
createFlight(@Body() flight: FlightDto): void {
this.flightService.createFlight(flight);
}
}
Damit diese Vorgehensweise im TypeScript strict mode funktioniert, muss in der tsconfig.json unter compilerOptions die Option "strictPropertyInitialization": false gesetzt werden.
Der deklarative Ansatz macht den Code sehr gut lesbar, und die direkte Integration in Nest.js erleichtert die Anwendung. Doch es gibt auch Schattenseiten: Die DTO-Klassen lassen sich im Frontend nur bedingt nutzen, da class-validator und class-transformer die Bundle-Size stark erhöhen und so die Performance beeinträchtigen können. Hinzu kommt, dass viele State-Management-Libraries im Frontend von Klassen abraten, um die Serialisierbarkeit der Daten zu gewährleisten – das gilt etwa für NgRx in Angular.
Validierung mit zod in Nest.js
Die Validierungsbibliothek zod ist in der TypeScript-Community in letzter Zeit stark in den Fokus gerückt. Sie verfolgt einen umgekehrten Ansatz: Zuerst schreiben wir den Validierungs-Code, und die Bibliothek leitet daraus die Typen vollautomatisch ab. Dafür setzt sie auf fortgeschrittene TypeScript-Features und generiert die Typen direkt aus dem Validierungs-Code. Für unser Beispiel könnte das so aussehen:
import { z } from 'zod';
export const flightSchema = z
.object({
from: z.string().min(5).max(100),
to: z.string().min(5).max(100),
time: z.string().datetime(),
delayed: z.boolean(),
})
.required();
export type Flight = z.infer<typeof flightSchema>;
Mit den Funktionen aus zod definieren wir also die komplette Struktur unseres Objekts und versehen sie im selben Schritt mit den Validierungsregeln. Den zugehörigen Type generiert zod am Ende über die Hilfsfunktion z.infer automatisch. Dieser Type Flight lässt sich über eine Nx lib im gesamten Projekt teilen – sowohl im Frontend als auch im Backend. Ein Angular-Frontend kann damit beispielsweise einen neuen Flug folgendermaßen anlegen:
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Flight } from '@angular-nestjs-zod/shared/util/api-models';
@Injectable({
providedIn: 'root',
})
export class FlightService {
private readonly httpClient = inject(HttpClient);
getFlights(): Observable<Flight[]> {
return this.httpClient.get<Flight[]>('/api/flights');
}
createFlight(newFlight: Flight): Observable<Flight> {
return this.httpClient.post<Flight>('/api/flights', newFlight);
}
}
Auf der Typebene kann TypeScript nun garantieren, dass nur ein gültiger Flug übergeben wird:

Es fehlt allerdings noch ein wichtiger Baustein: Damit die Validierung zur Laufzeit greift und Regeln wie minLength und maxLength angewendet werden, muss eine eigene ValidationPipe implementiert und registriert werden. Das ist mit wenigen Zeilen Code umgesetzt:
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
import { ZodSchema } from 'zod';
@Injectable()
export class ZodValidationPipe implements PipeTransform {
constructor(private readonly schema: ZodSchema) {}
transform(value: unknown) {
try {
return this.schema.parse(value);
} catch (error) {
throw new BadRequestException(error);
}
}
}
Im Controller wird die Pipe dann eingebunden, um die Validierung für einen bestimmten Request zu aktivieren:
import { Body, Controller, Get, Post, UsePipes } from '@nestjs/common';
import { FlightService } from './flight.service';
import { ZodValidationPipe } from '../common/zod-validation-pipe';
import { Flight, flightSchema } from '@angular-nestjs-zod/shared/util/api-models';
@Controller('flights')
export class FlightController {
constructor(private readonly flightService: FlightService) {}
@Get()
getFlights(): Flight[] {
return this.flightService.getAllFlights();
}
@Post()
@UsePipes(new ZodValidationPipe(flightSchema))
createFlight(@Body() flight: Flight): void {
this.flightService.createFlight(flight);
}
}
Sendet man nun ein ungültiges Objekt an die API, sieht man den folgenden Fehler:


Das zeigt: Die Validierung funktioniert sowohl auf Typebene als auch zur Laufzeit.
Fazit
Mit zod lassen sich Typen und Validierungsregeln in einem einzigen Schritt erstellen – schlank, einfach und gut lesbar. Auch die Einbindung in Nest.js gelingt mit nur wenigen Zeilen Code. Die Sicherheit und Stabilität von Anwendungen lässt sich dadurch deutlich verbessern. Gerade Anwendungen mit komplexer fachlicher Logik, die umfangreiche Validierung erfordern, profitieren von diesem Ansatz. Außerdem fällt die Vergrößerung des Frontend-Bundles durch zod in der Regel deutlich geringer aus als bei einem Einsatz von class-validator und class-transformer.
Quellen
- Nest.js Dokumentation - ValidationPipe
- Nest.js Dokumentation - Custom Validation with zod
- zod Dokumentation
- Bundlephobia class-validator
- Bundlephobia class-transformer
- Bundlephobia zod
Autoren
- Thomas Enderle
- Marco Hämmerle
- Florian Tischler
