Startseite / Artikel

Serverseitige Eingabenvalidierung in Express mit express-validator Chains

Erfahren Sie, warum Express-APIs die Eingaben auf dem Server validieren müssen, wie express-validator-Ketten Fehler für validationResult erfassen und wie man eine Produktionsroute strukturiert.

Verlag: REACTAPP.TOP10 Min. Lesezeit2373 Wörter

Quellen: Stop Trusting User Input: A Beginner’s Guide to `express-validator` in Express.js

Erlaubt eine Express API die Eingabe, erhält sie früher oder später etwas wie "age": "banana"; der Server stürzt entweder ab oder, was noch schlimmer ist, speichert diesen Wert. Die Validierung ist die Schicht, die das verhindert – und eine gute Umsetzung im Backend ist unerlässlich, egal wie sorgfältig die Formulare im Frontend gestaltet sind. Dieser Leitfaden erklärt, was Validierung bedeutet und warum der Server dafür verantwortlich ist, zeigt den manuellen Ansatz sowie seine Grenzen und erläutert anschließend, wie express-validator intern funktioniert, welche Methoden am häufigsten verwendet werden und wie eine realistische Route Validierung, Sanitierung sowie datenbankbasierte Überprüfungen kombiniert.

Was Validierung bedeutet

Validierung ist der Vorgang, bei dem festgestellt wird, ob die eingehenden Daten die erwartete Struktur und den Inhalt aufweisen, bevor Ihr Code darauf reagiert. Eine nützliche Analogie ist die Überprüfung der Ausweise an einem Eingang: Es spielt keine Rolle, wie zuversichtlich jemand aussieht – das Personal prüft den Ausweis, und wenn er nicht gültig ist, darf die Person nicht hinein. Die Validierung wendet dieselbe Sorgfalt auf jedes Feld einer Anfrage an. Ist das tatsächlich eine E-Mail-Adresse? Ist das wirklich eine Zahl? Gibt es dieses Feld überhaupt?

Betrachten Sie einen Registrierungsendpunkt. Ein gut strukturiertes Anfrage-Body könnte so aussehen:

{
  "name": "Rahul Sharma",
  "email": "rahul@example.com",
  "age": 25,
  "password": "supersecret123"
}

Die Validierung bedeutet, zu überprüfen, ob email in der Form einer E-Mail-Adresse formatiert ist, ob age eine Zahl und nicht Text ist sowie ob password eine Mindestlänge aufweist, die sie von einer rein formellen Angabe unterscheidet.

Warum sich das Backend nicht auf Prüfungen am Frontend verlassen kann

Ein häufiges Einwand ist, dass das Anmeldeformular die Felder bereits als required markiert und den E-Mail-Format überprüft. Das reicht jedoch nicht aus, denn aus Sicht eines Angreifers ist die Benutzeroberfläche optional. Jeder kann Ihre API direkt mit Postman, curl oder einem Skript aufrufen und alle Regeln in Ihrer Benutzeroberfläche umgehen. Wenn der Server die Daten selbst nicht überprüft, vertraut er im Grunde jedem, der die Anfrage sendet.

Die konkreten Vorteile der Serverseitigen Validierung:

  • Es schützt Ihre Daten. Wenn eine Spalte eine Zahl erwartet und stattdessen "twenty-five" erhält, entsteht entweder ein Fehler oder, schlimmer noch, eine fehlerhafte Datenspur, die später zu Fehlern führt.
  • Es verringert Ihre Angriffsfläche. Unüberprüfte Eingaben sind ein häufiger Eintrittspunkt für Injection-Angriffe wie SQL-Injection und Cross-Site-Scripting (XSS). Validierung und Sanitierung verringern diese Exponiertheit, ersetzen jedoch nicht die Verwendung parametrisierter Abfragen sowie eine ordnungsgemäße Ausgabekodierung.
  • Es verhindert verwirrende Fehlermeldungen. Viele Produktionsfehler lassen sich auf ein fehlendes, leeres oder falsch eingegebenes Feld zurückführen, das einige Zeilen vor dem eigentlichen Fehler verwendet wurde.
  • Es liefert nützliche Fehlermeldungen. Anstelle einer allgemeinen Meldung wie „Etwas ist schiefgelaufen“ kann den Clients genau mitgeteilt werden, was zu korrigieren ist – beispielsweise „Die E-Mail ist ungültig“ oder „Das Passwort muss mindestens 8 Zeichen lang sein“.
  • Die Arbeitsteilung ist einfach: Die Frontend-Validierung dient dazu, den Benutzern zu helfen, während die Backend-Validierung dazu dient, die Anwendung zu schützen. Beide sind wertvoll, doch nur die Backend-Schicht ist zwingend erforderlich.

    Manuelle Validierung und warum sie das Skalieren behindert

    Es lohnt sich, zunächst die manuelle Version anzusehen. Hier ist der Beginn einer Registrierungsroute, die die Felder aus dem parsierten Body abruft (dabei wird angenommen, dass ein JSON-Body-Parser wie express.json() registriert ist):

    app.post("/register", (req, res) => {
      const { name, email, age, password } = req.body;
    

    Jedes Feld erhält anschließend seine eigene Schutzklausel, die eine 400-Antwort zurückgibt:

      if (!name || typeof name !== "string" || name.trim().length === 0) {
        return res.status(400).json({ error: "Name is required" });
      }  if (!email || !email.includes("@")) {
        return res.status(400).json({ error: "A valid email is required" });
      }  if (!age || typeof age !== "number" || age < 18) {
        return res.status(400).json({ error: "Age must be a number and at least 18" });
      }  if (!password || password.length < 8) {
        return res.status(400).json({ error: "Password must be at least 8 characters" });
      }  // If we made it here, the data is safe to use
      res.json({ message: "User registered successfully" });
    });
    

    Für eine kleine Anwendung ist das völlig in Ordnung. Wenn sich jedoch der Codebestand vergrößert, häufen sich die Probleme:

    • Jede Route, die Eingaben entgegennimmt, benötigt ihren eigenen Block an if-Anweisungen.
    • Die Überprüfungen werden zwischen den Routen kopiert, und eine in einer Kopie vorgenommene Korrektur wird leicht in den anderen übersehen.
  • Die eigentliche Geschäftslogik zur Registrierung des Benutzers ist hinter dem Validierungscode verborgen.
  • Es wird schnell mühsam, jeden möglichen Fall manuell abzudecken – wie leere Zeichenketten, null, falsche Typen sowie Werte, die zu kurz oder zu lang sind.
  • Auch die manuelle Version weist subtile Mängel auf. email.includes("@") akzeptiert viele ungültige Adressen, und der Aufruf von .includes auf einem nicht-stringigen Wert führt zu Fehlern. Solche wiederholten, fehleranfälligen Überprüfungen sind genau das, was eine Bibliothek übernehmen sollte.

    Was express-validator bietet

    express-validator ist eine Sammlung von Express-Middlewares, die auf der validator.js-Bibliothek basieren. Anstatt für jedes Feld konditionale Logik schreiben zu müssen, deklarieren Sie Regeln, und die Bibliothek prüft die Anfragedaten daran. Beginnen Sie damit, die beiden Hauptfunktionen zu importieren:

    const { body, validationResult } = require("express-validator");
    

    Die gleiche Registrierungsroute, neu geschrieben mit deklarativen Regeln, sieht so aus:

    app.post(
      "/register",
      [
        body("name").trim().notEmpty().withMessage("Name is required"),
        body("email").isEmail().withMessage("A valid email is required"),
        body("age").isInt({ min: 18 }).withMessage("Age must be at least 18"),
        body("password").isLength({ min: 8 }).withMessage("Password must be at least 8 characters"),
      ],
      (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
          return res.status(400).json({ errors: errors.array() });
        }    res.json({ message: "User registered successfully" });
      }
    );
    

    Die Regeln sind nun in nahezu alltäglicher Sprache formuliert: Der Name darf nach Trimmen nicht leer sein, die E-Mail muss eine echte E-Mail-Adresse sein, das Alter muss eine ganze Zahl von mindestens 18 sein und das Passwort muss mindestens acht Zeichen lang sein. Der Handler am Ende kümmert sich nur um eine Frage: Ist etwas schiefgelaufen?

    Was passiert, wenn eine Anfrage eintrifft

    Der häufigste Missverständnispunkt bei Anfängern ist die Erwartung, dass express-validator fehlerhafte Anfragen automatisch ablehnt. Das tut es nicht. Es sammelt Informationen darüber, was falsch ist, und überlässt die Antwort vollständig Ihnen. Die Abfolge für jede Anfrage ist:

    • Express startet die für die Route definierte Middleware-Kette und führt jede Eintragung nacheinander aus.
  • Jede Validierungskette läuft als Middleware. Wenn body("email").isEmail() ausgeführt wird, liest es den Wert von email aus req.body und prüft ihn. Ein Fehler führt weder zu einem Abbruch noch zum Beenden der Anfrage; die Kette protokolliert den Fehler im Anfragedatumensatz und geht weiter.
  • Jede Regel erhält ihre Chance. Weil eine fehlerhafte Kette dennoch die nächste Middleware aufruft, werden alle Regeln ausgeführt und jedes Problem protokolliert – nicht nur das erste.
  • validationResult(req) liest die protokollierten Fehler. Es handelt sich dabei um eine gewöhnliche Funktion, keine Middleware, sodass man sie innerhalb des Handlers aufruft. Sie validiert nichts selbst; sie gibt lediglich wieder, was die Ketten bereits gesammelt haben.
  • Ihr Code bestimmt das Ergebnis. Wenn errors.isEmpty() false zurückgibt, senden Sie eine 400-Antwort mit den Details. Andernfalls gehen Sie zur eigentlichen Bearbeitung über.
  • Das Design bevorzugt Flexibilität gegenüber Bequemlichkeit. Da eine Ablehnung nicht automatisch erfolgt, haben Sie die Kontrolle über das Antwortformat und den Zeitpunkt sowie können zwischen den Überprüfungen und der endgültigen Entscheidung weitere Logik ausführen. Der Kompromiss besteht darin, dass das Vergessen, validationResult() aufzurufen, die Validierung für diesen Pfad stillschweigend deaktiviert – deshalb wickeln viele Teams die Überprüfung in ein kleines, wiederverwendbares Middleware-Paket ein.

    Die vier Bausteine

    Sobald Sie die einzelnen Komponenten kennen, ist die Bibliothek einfach zu verstehen.

    Validierungsketten

    Funktionen wie body(), query(), param() und check() bilden eine Kette: eine Abfolge von Regeln, die mit Punkten verbunden sind, wie z. B. body("email").trim().isEmail(). Jede davon bezieht sich auf einen anderen Teil der Anfrage:

    • body() liest req.body, den JSON- oder Formdateninhalt.
    • query() liest req.query, die Abfragesuche nach dem ?, wie in ?page=2.
    • param() liest req.params, Routenparameter wie :id in /users/:id.
    • check() prüft alle Bereiche der Anfrage gleichzeitig.

    Validatoren

    Validatoren sind Funktionen, die einen Wert überprüfen: isEmail(), isInt(), isLength(), isAlphanumeric() und viele weitere. Die meisten verweisen auf validator.js, das bereits ausgereifte, gut getestete Logik für gängige Formate enthält.

    Sanitisierer

    Sanitisierer wandeln Daten um, anstatt sie nur zu überprüfen. trim() entfernt umliegenden Leerlauf, normalizeEmail() wandelt eine E-Mail-Adresse in eine kanonische Form um, und escape() ersetzt HTML-relevante Zeichen durch ihre entsprechenden Entity-Formen, sodass sie nicht als Markup interpretiert werden können. Sanitisierer können zusammen mit Validatoren in derselben Kette verwendet werden, wobei die Reihenfolge wichtig ist: Ein trim(), das vor notEmpty() platziert wird, bedeutet, dass ein Wert, der nur aus Leerzeichen besteht, fehlschlägt.

    Der Ergebnisspeicher pro Anfrage

    Sobald eine Kette ausgeführt wird, werden ihre Fehler dem jeweiligen req-Objekt zugeordnet. validationResult(req) liest einfach aus diesem Speicher ab, weshalb es dasselbe Anfrageobjekt benötigt, mit dem die Ketten verarbeitet wurden.

    Insgesamt sieht der Ablauf so aus: Eine Anfrage trifft ein, die Ketten werden ausgeführt und erfassen leise Probleme, der Handler fragt nach den Ergebnissen, und Sie wählen die Antwort aus. Wenn Sie diesen Ansatz mit der schema-basierten Validierung vergleichen möchten, zeigt unser Vergleich von Zod und express-validator die Vor- und Nachteile auf.

    Die Methoden, die die meisten echten Projekte abdecken

    Ihnen ist nicht die gesamte API erforderlich. Eine kleine Anzahl davon reicht aus, um die meisten Fälle zu bewältigen.

    Ortsspezifische Selektoren

    • body(field) für Felder im Anfragekörper
    • query(field) für Parameter in der Abfragesuche
  • param(field) für Route-Parameter
  • Häufig verwendete Validatoren

    • .notEmpty(): Der Wert darf nicht leer sein
    • .isEmail(): Der Wert muss eine gültige E-Mail-Adresse sein
    • .isInt({ min, max }): Der Wert muss eine ganze Zahl sein, optional innerhalb bestimmter Grenzen
    • .isLength({ min, max }): Überprüft die Länge einer Zeichenkette
    • .isString(): Der Wert muss eine Zeichenkette sein
    • .isBoolean(): Der Wert muss ein boolescher Wert sein
    • .matches(regex): Der Wert muss einem benutzerdefinierten Muster entsprechen
    • .isIn([...values]): Der Wert muss sich aus einer festgelegten Menge befinden, nützlich für Felder wie status, das entweder "active" oder "inactive" sein kann

    Häufig verwendete Sanitizer

    • .trim(): Entfernt Leerzeichen am Anfang und Ende.
    • .escape(): Entlädt HTML-Zeichen, um das Risiko von Einbrüchen zu verringern.
    • .normalizeEmail(): Standardisiert die Formatierung von E-Mails.
    • .toInt() und .toFloat(): Wandeln Eingabestringe in Zahlen um.

    Benutzerdefinierte Regeln

    • .custom((value, { req }) => { ... }): Implementiert eigene Regeln, wenn kein eingebautes Validierungsverfahren passt – beispielsweise zur Überprüfung, ob ein Benutzername bereits registriert ist.

    Fehlerberichterstattung

    • .withMessage("text"): Legt die Nachricht für die vorherige Regel fest.
    • validationResult(req): Sammelt alle Fehler für die aktuelle Anfrage.
    • errors.array(): Wandelt diese Fehler in ein einfaches Array um, das für eine JSON-Antwort geeignet ist.

    Eine Produktionsroute

    Das folgende Beispiel entspricht eher dem, was eine echte API enthält: Eine Route zur Erstellung eines Blogbeitrags, die Validierung, Sanitisierung sowie eine Abfrage der Datenbank kombiniert. Zunächst die Imports, wobei Post für ein Modell wie ein Mongoose-Schema steht:

    const express = require("express");
    const { body, validationResult } = require("express-validator");
    const Post = require("./models/Post"); // a Mongoose model, for example
    

    Dann der Router sowie die Route mit ihrem Regelarray:

    const router = express.Router();router.post(
      "/posts",
      [
        body("title")
          .trim()
          .notEmpty().withMessage("Title is required")
          .isLength({ max: 100 }).withMessage("Title cannot exceed 100 characters"),    body("slug")
          .trim()
          .notEmpty().withMessage("Slug is required")
          .matches(/^[a-z0-9-]+$/).withMessage("Slug can only contain lowercase letters, numbers, and hyphens")
          .custom(async (slug) => {
            const existing = await Post.findOne({ slug });
            if (existing) {
              throw new Error("This slug is already taken");
            }
            return true;
          }),    body("content")
          .trim()
          .notEmpty().withMessage("Content cannot be empty")
          .isLength({ min: 20 }).withMessage("Content should be at least 20 characters"),    body("tags")
          .optional()
          .isArray().withMessage("Tags must be an array")
          .custom((tags) => tags.every((t) => typeof t === "string"))
          .withMessage("Each tag must be a string"),
      ],
      async (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
          return res.status(400).json({ errors: errors.array() });
        }    try {
          const post = await Post.create(req.body);
          res.status(201).json(post);
        } catch (err) {
          res.status(500).json({ error: "Something went wrong while saving the post" });
        }
      }
    );module.exports = router;
    

    Mehrere Details sind es wert, genauer untersucht zu werden:

    • Regeln, die auf der Datenbank basieren. Die slug-Logik verwendet einen asynchronen .custom()-Validierer, um bestehende Beiträge abzurufen. Eine Regex kann das Format eines Slugs überprüfen, doch nur eine Abfrage kann sicherstellen, dass er einzigartig ist. Das Werfen eines Fehlers innerhalb der Custom-Funktion markiert das Feld als ungültig mit dieser Meldung.
  • Optionale Felder, die dennoch überprüft werden. tags ist mit .optional() markiert, sodass ein Beitrag ohne Tags durchkommt. Werden jedoch Tags bereitgestellt, müssen diese ein Array von Zeichenketten sein.
  • Ein fokussierter Handler. Der Route-Handler hat zwei Aufgaben: Er meldet Validierungsfehler und speichert den Beitrag. Die gesamte Logik zur Überprüfung, ob die Daten akzeptabel sind, befindet sich im Regelarray, wo sie leicht lesbar und änderbar ist.
  • Zwei Hinweise, bevor man dieses Muster kopiert. Erstens ist eine Einzigartigkeitsprüfung in einem Validator anfällig für Race Conditions: Zwei Anfragen mit demselben Slug können beide die Prüfung bestehen, bevor eine gespeichert wird; daher sollte man einen eindeutigen Index in der Spalte führen und auch den Fehler bei doppelten Schlüsseln behandeln. Zweitens speichert das Direktübergeben von req.body an Post.create() alle zusätzlichen Felder, die der Client mitgeliefert hat – nicht nur die validierten. express-validator bietet matchedData(req) an, das nur die von den Validierungsregeln abgedeckten Felder zurückgibt; dessen Verwendung schließt diese Lücke bei der Massenzuweisung.

    Sobald die Anwendung wächst, sollten die Regelarrays in eigene Module verlegt werden, beispielsweise validators/postValidator.js, und in die Route-Dateien importiert werden, damit die Routen nicht zu einem Meer aus Validierungscode werden.

    Zusammenfassung

    Die Validierung ist kein optionales Extra; sie bestimmt, ob Ihre Anwendung den von ihr verarbeiteten Daten vertrauen kann. Handgeschriebene if-Prüfungen eignen sich für wenige Felder, leiden aber schnell bei vielen Routen.

    express-validator ist kein Wundermittel. Es führt Ihre deklarierten Regeln als Middleware aus, protokolliert Fehler in der Anfrage und ermöglicht es validationResult(), Ihnen einen Bericht zur Verfügung zu stellen, damit Sie die Antwort entscheiden können. Angesichts dieses Ablaufs ist der Rest der Bibliothek ein Werkzeugkasten mit Validatoren und Sanitizern, aus dem Sie nach Bedarf wählen können. Die wichtigsten Punkte in der Praxis sind:

    • Betrachten Sie die Backend-Validierung als verpflichtend, unabhängig von Frontend-Prüfungen.
    • Rufen Sie immer validationResult() auf, idealerweise aus einer gemeinsamen Middleware, denn die Bibliothek lehnt niemals von selbst ab.
    • Ordnen Sie die Sanitizer bewusst in einer Kette an.
  • Einzigartigkeitsprüfer für Rückrufe mit Datenbankbeschränkungen.
  • Speichern Sie matchedData(req) anstelle des rohen Inhalts.