Walidacja danych wejściowych po stronie serwera w Express za pomocą łańcuchów express-validator
Dowiedz się, dlaczego API Express muszą weryfikować dane wejściowe na serwerze, jak łańcuchy express-validator rejestrują błędy dla validationResult oraz jak skonstruować trasę produkcyjną.
Źródła: Stop Trusting User Input: A Beginner’s Guide to `express-validator` in Express.js
Ranо czy późno każda API Express, która przyjmuje dane wejściowe, otrzymuje coś w rodzaju "age": "banana", a serwer albo się zatrzymuje, albo, co gorsza, przechowuje te dane. Walidacja jest warstwą, która temu zapobiega, a jej prawidłowe zastosowanie po stronie backendu ma znaczenie niezależnie od tego, jak staranne są formularze po stronie frontendu. Ten przewodnik wyjaśnia, czym jest walidacja i dlaczego serwer musi nią zarządzać, pokazuje podejście ręczne oraz jego ograniczenia, a następnie opisuje, jak express-validator działa wewnętrznie, które metody będą najczęściej używane oraz jak realistyczna trasa łączy walidację, oczyszczanie danych i sprawdzania oparte na bazie danych.
Czym jest walidacja
Walidacja to proces potwierdzania, że przychodzące dane mają taką formę i treść, jakich oczekujemy, zanim Twój kod na nie zadziała. Przydatną analogią jest kontrola biletów we wejściu: nie ma znaczenia, jak pewny siebie ktoś wygląda – pracownicy sprawdzają jego dokument tożsamości, a jeśli nie jest on prawidłowy, osoba ta nie może wejść. Walidacja stosuje taką samą staranność do każdego pola żądania. Czy to rzeczywiście adres e-mail? Czy to naprawdę liczba? Czy w ogóle istnieje to pole?
Rozważmy punkt końcowy do rejestracji. Dobrze sformatowany treść żądania może wyglądać w ten sposób:
{
"name": "Rahul Sharma",
"email": "rahul@example.com",
"age": 25,
"password": "supersecret123"
}
Jego walidacja oznacza potwierdzenie, że email jest sformatowany jak adres e-mail, że age to liczba, a nie tekst, oraz że password ma minimalną długość, która sprawia, że nie jest to tylko formalność.
Dlaczego backend nie może polegać na kontrolach frontendu
Częstym zarzutem jest to, że formularz rejestracyjny już oznacza pola jako required i sprawdza format adresu e-mail w przeglądarce. To nie wystarcza, ponieważ z punktu widzenia atakującego interfejs użytkownika jest opcjonalny. Każdy może bezpośrednio skorzystać z twojej API za pomocą Postman, curl lub skryptu i obejść wszystkie zasady w twoim interfejsie. Jeśli serwer nie weryfikuje samych danych, faktycznie ufa każdemu, kto wysyła żądanie.
Konkretne korzyści z walidacji po stronie serwera:
- Ochronia twoje dane. Gdy kolumna oczekuje liczby, a otrzymuje
"twenty-five", pojawia się błąd lub, co gorsza, nieprawidłowy rekord, który powoduje błędy później.
Rozdział pracy jest prosty: walidacja frontendu służy do pomocy użytkownikom, natomiast walidacja backendu ma na celu ochronę aplikacji. Obie są cenne, ale tylko warstwa backendowa jest obowiązkowa.
Walidacja ręczna i dlaczego utrudnia skalowanie
Warto najpierw zobaczyć wersję ręczną. Oto początek trasy rejestracji, która pobiera pola z skonwertowanego ciała żądania (przy założeniu, że zarejestrowano parser ciała w formacie JSON, np. express.json()):
app.post("/register", (req, res) => {
const { name, email, age, password } = req.body;
Każde pole otrzymuje następnie własną klauzulę ochronną, która zwraca odpowiedź 400:
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" });
});
Dla małej aplikacji jest to w pełni akceptowalne. Jednak w miarę rozwoju bazy kodu problemy się gromadzą:
- Każda trasa, która przyjmuje dane wejściowe, potrzebuje własnego bloku instrukcji
if. - Kontrole są kopiowane między trasami, a naprawa zastosowana w jednej kopii może zostać łatwo przeoczona w pozostałych.
null, niewłaściwe typy oraz wartości zbyt krótkie lub zbyt długie, szybko staje się męczące.Wersja ręczna ma również subtelne braki. Funkcja email.includes("@") akceptuje wiele nieprawidłowych adresów, a wywołanie .includes na wartości innej niż łańcuch powoduje błąd. Tego typu powtarzalne i podatne na błędy sprawdzania to dokładnie to, co powinna obsłużyć biblioteka.
Co oferuje express-validator
express-validator to zbiór middleware dla Express, stworzony na bazie biblioteki validator.js. Zamiast pisać logikę warunkową dla każdego pola, deklarujesz reguły, a biblioteka sprawdza dane z żądania pod kątem tych reguł. Zacznij od importowania dwóch głównych funkcji:
const { body, validationResult } = require("express-validator");
Ta sama ścieżka rejestracji, przepisana za pomocą reguł deklaratywnych, wygląda w ten sposób:
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" });
}
);
Reguły są teraz sformułowane w języku bliskim zwykłej mowy: nazwa nie może być pusta po wycięciu zbędnych znaków, adres e-mail musi być prawidłowym adresem e-mail, wiek musi być liczbą całkowitą co najmniej 18, a hasło musi składać się z co najmniej ośmiu znaków. Funkcja obsługująca na końcu zajmuje się tylko jednym pytaniem: czy coś poszło nie tak?
Co się dzieje, gdy przychodzi żądanie
Najczęstszym błędem początkujących jest oczekiwanie, że express-validator automatycznie odrzuci błędne żądania. Tak nie jest – zbiera informacje na temat problemu i pozostawia odpowiedź całkowicie tobie. Sekwencja działań dla każdego żądania jest następująca:
- Express uruchamia łańcuch middleware zdefiniowany dla danej ścieżki i wykonywa każdy element w kolejności.
body("email").isEmail(), odczytuje ona wartość email z req.body i ją sprawdza. Niepowodzenie nie powoduje rzucenia błędu ani zakończenia żądania; łańcuch rejestruje błąd w obiekcie żądania i przechodzi dalej.validationResult(req) odczytuje zarejestrowane błędy. Jest to zwykła funkcja, a nie middleware, więc wywołuje się ją wewnątrz obsługownika żądania. Sama nie wykonuje żadnej walidacji; jedynie podaje informacje zebrane już przez łańcuchy.errors.isEmpty() zwraca false, wysyłasz odpowiedź 400 z szczegółami. W przeciwnym razie przechodzisz do rzeczywistej pracy.Konstrukcja ta faworyzuje elastyczność nad wygodą. Ponieważ odrzucenie nie jest automatyczne, kontrolujesz format i czas odpowiedzi, a także możesz wykonać inną logikę pomiędzy sprawdzaniami a ostateczną decyzją. Wadą jest to, że zapomnienie o wywołaniu validationResult() w tajemnicy dezaktywuje walidację dla danej ścieżki, dlatego wiele zespołów otacza to sprawdzenie małym, wielokrotnie używalnym middleware’em.
Cztery elementy budulcowe
Gdy już znasz poszczególne elementy, biblioteka staje się prosta w użyciu.
Łańcuchy walidacji
Funkcje takie jak body(), query(), param() i check() tworzą łańcuch: sekwencję reguł połączonych kropkami, na przykład body("email").trim().isEmail(). Każda z nich odnosi się do innej części żądania:
body()odczytujereq.body, zawartość w formacie JSON lub formularza.query()odczytujereq.query, ciąg zapytania po znaku?, jak w przykładzie?page=2.param()odczytujereq.params, parametry trasy, takie jak:idw adresie/users/:id.check()sprawdza wszystkie miejsca w żądaniu jednocześnie.
Walidatory
Walidatory to funkcje, które sprawdzają wartość: isEmail(), isInt(), isLength(), isAlphanumeric() oraz wiele innych. Większość z nich odwołuje się do pliku validator.js, który zawiera już sprawdzoną logikę do obsługi powszechnych formatów.
Sanitizatory
Sanitizatory przekształcają dane, a nie tylko je sprawdzają. Funkcja trim() usuwa otaczające białe przestrzenie, normalizeEmail() konwertuje adres na formę kanoniczną, a escape() zastępuje znaki istotne dla HTML ich odpowiednikami w postaci entytetów, aby nie mogły być interpretowane jako elementy składniowe. Sanitizatory można łączyć z walidatorami w tej samej łańcuchu, przy czym kolejność ma znaczenie: trim() umieszczone przed notEmpty() oznacza, że wartość składająca się wyłącznie z przestrzeni zostanie odrzucona.
Miejsce przechowywania wyników na żądanie
Gdy tylko uruchamiana jest łańcuchowa walidacja, jej błędy są przypisywane do konkretnego obiektu req. Funkcja validationResult(req) po prostu odczytuje dane z tego źródła, dlatego potrzebuje tego samego obiektu żądania, który został przetworzony przez łańcuchy.
W skrócie, proces wygląda tak: przychodzi żądanie, łańcuchy są uruchamiane i cicho rejestrują problemy, a obsługa żądania prosi o wyniki, po czym decydujesz o odpowiedzi. Jeśli rozważasz tę metodę w porównaniu z walidacją opartą na schematach, nasze porównanie Zod i express-validator omawia zalety i wady obu podejść.
Metody, które obejmują większość rzeczywistych projektów
Nie potrzebujesz całej API – mała grupa metod wystarcza do obsługi większości przypadków.
Wybieracze lokalizacji
body(field)– do pól w ciele żądaniaquery(field)– do parametrów w ciągu zapytania
param(field) dla parametrów trasyPowszechne walidatory
.notEmpty(): wartość nie może być pusta.isEmail(): wartość musi być prawdopodobnym adresem e-mail.isInt({ min, max }): wartość musi być liczbą całkowitą, opcjonalnie w określonych granicach.isLength({ min, max }): sprawdza długość ciągu znaków.isString(): wartość musi być łańcuchem.isBoolean(): wartość musi być wartością logiczną.matches(regex): wartość musi pasować do specyficznego wzorca.isIn([...values]): wartość musi być jedną z ustalonej listy, przydatne dla pól takich jakstatus, które może mieć wartość"active"lub"inactive"
Powszechne oczyszczacze
.trim(): usuwa białe przestrzenie na początku i końcu.escape(): escapeuje znaki HTML w celu zmniejszenia ryzyka iniekcji.normalizeEmail(): standaryzuje format e-maili.toInt()i.toFloat(): konwertują dane tekstowe na liczby
Reguły niestandardowe
.custom((value, { req }) => { ... }): umożliwia zaimplementowanie własnej reguły, gdy żaden wbudowany walidator nie pasuje, na przykład w celu sprawdzenia, czy nazwa użytkownika nie jest już zarejestrowana
Raportowanie błędów
.withMessage("text"): ustawia komunikat dla poprzedniej regułyvalidationResult(req): zbiera wszystkie błędy dotyczące aktualnej żądaniaerrors.array(): przekształca te błędy w zwykły tablicę nadającą się do odpowiedzi JSON
Trasa w stylu produkcyjnym
Poniższy przykład jest bliższy temu, co zawiera rzeczywista API: trasa, która tworzy wpis na blogu, łącząca walidację, oczyszczanie danych oraz sprawdzenie w bazie danych. Najpierw importy, gdzie Post oznacza model taki jak schemat Mongoose:
const express = require("express");
const { body, validationResult } = require("express-validator");
const Post = require("./models/Post"); // a Mongoose model, for example
Następnie router oraz trasa wraz z jej tablicą reguł:
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;
Warto przyjrzeć się kilku szczegółom:
- Reguły oparte na bazie danych. Łańcuch
slugwykorzystuje asynchronicznego walidatora.custom(), aby sprawdzić istnienie już istniejących wpisów. Reguła regex może potwierdzić format slug, ale tylko zapytanie do bazy danych może potwierdzić jego unikalność. Rzucenie błędu wewnątrz funkcji custom oznacza pole jako nieważne z tym komunikatem.
tags jest oznaczone jako .optional(), więc wpis bez tagów zostanie przyjęty, ale jeśli tagi są podane, muszą stanowić tablicę ciągów znaków.Dwie uwagi przed skopiowaniem tego wzorca. Po pierwsze, sprawdzanie unikalności w walidatorze może ulec konfliktowi: dwa żądania z tym samym identyfikatorem mogą oba przejść weryfikację przed zapisaniem, więc należy utrzymywać unikalny indeks w kolumnie i obsługiwać również błąd duplikatu klucza. Po drugie, przekazanie bezpośrednio req.body do Post.create() zapisuje wszystkie dodatkowe pola dostarczone przez klienta, a nie tylko te zweryfikowane. express-validator oferuje metodę matchedData(req), która zwraca tylko te pola, które zostały objęte Twoimi łańcuchami walidacji, a jej użycie eliminuje ten problem masowego przypisywania.
Gdy aplikacja rośnie, przenieś tablice reguł do dedykowanych modułów, na przykład validators/postValidator.js, i importyj je do plików tras, aby trasy nie zamieniały się w góry kodu walidacyjnego.
Zakończenie
Walidacja nie jest opcjonalnym dodatkiem; decyduje o tym, czy twoja aplikacja może ufać danym, które przetwarza. Ręczne sprawdzanie za pomocą instrukcji if nadaje się dla niewielu pól, ale szybko traci efektywność przy wielu trasach.
express-validator to nie magia. Uruchamia zadeklarowane reguły jako middleware, rejestruje błędy w żądaniu i umożliwia użycie funkcji validationResult(), aby otrzymać raport i podjąć decyzję dotyczącą odpowiedzi. Biorąc to pod uwagę, reszta biblioteki stanowi zestaw narzędzi w postaci walidatorów i oczyszczaczy, które można wybrać w zależności od potrzeb. Najważniejsze kwestie w praktyce to:
- Traktuj walidację w tle jako obowiązkową, niezależnie od sprawdzeń na stronie klienta.
- Zawsze wywołuj funkcję
validationResult(), najlepiej z jednego wspólnego middleware, ponieważ biblioteka sama w sobie nigdy nie odrzuca żądania. - Ustalaj kolejność działania oczyszczaczy w łańcuchu celowo.
matchedData(req) zamiast surowego treści żądania.Literatura pokrewna
- Usuwanie boilerplate try/catch z tras Express za pomocą asyncHandler — Dowiedz się, jak middleware obsługujące błędy, obudowa asyncHandler oraz spersonalizowana klasa AppError przenoszą odpowiedzi na błędy w Express z każdej trasy do jednego spójnego miejsca.
- Kontrola dostępu oparta na rolach w Express z JWT i dwoma middleware — Dowiedz się, jak wprowadzić kontrolę dostępu opartą na rolach w API Express poprzez połączenie middleware autoryzacji JWT z wielokrotnie używalną funkcją authorize(), oraz kiedy należy zwracać kod 401, a kiedy 403.