Парадакты аплікацыі з боку сервера ў Express за дапамою express-validator Chains
Дазвольце даклэ расказаць, чаму API-і Express павінны пераканаліваць даныя, якія прыходзяць, на серверае, як каскады express-validator фіксуюць адмешчанні для validationResult, і як структураваць маршрут для прыемлівання запытаў у працоўным режыме.
Крыніцы: Stop Trusting User Input: A Beginner’s Guide to `express-validator` in Express.js
Раней чым пазней кожны Express API, які прыймае даныя, атрымлівае ўсё гэта ў виглядзе "age": "banana"; сервер альбо зупіняецца, альбо, што ўжо горш, зберагае гэтыя даны. Праверкі — это той слой, який запобегае гэтаму, і ўжо незалежна ад таго, насколькі рэтельныя вашы формы на фронтендзе, важна правільна ўжоць на бэкендзе. У гэтым кяліку пояснюецца, што такое праверкі і чаму сервер павінен імі кераваць, показваецца ручны спосаб і таму, дзе ён не працюе, а таксама паследавана як express-validator працюе внутршняя, якія методы будуць вжываны найчащэй, і як реалістычны маршрут спаўнаюць праверкі, санітараванне і пераказы на адповідна база дадзеных.
Што значыць праверкі
Автаперакрыцце — это процэс парабярання таго, чыя прыходзячая мэтаданы маюць тую форму і змест, якія вы чакалі, прычыму ваш код не пачынае з іхім работаць. Корыстны аналагія — перакрыцце дзверэй у закладзе: неважна, насколькі адпаведальны выглядае чалавек, персонал перакрывае яго айдэнтыфікат, і якщо ён не пасваўае, ён не можа зайсці. Автаперакрыцце прыкладзае тое ж адгледжэнне кожнаму полю запита. Чы ўсё-такі гэта адрес электроннай пашты? Чы гэта сапраўды цячка? Чы ўзагалі існуе гэта поле?
Разглядзім канцэнтры регистрацыі. Коректна сформаваны тэла запита можа выглядаць так:
{
"name": "Rahul Sharma",
"email": "rahul@example.com",
"age": 25,
"password": "supersecret123"
}
Автаперакрыцце яго значыць парабярання таго, чы email сформаваны як адреса электроннай пашты, чы age — цячка, а не тэкст, і чы password мае минімальную дужыну, якая робіць яго не проста формальнасцю.
Чаму бэкенд не можа паспольгавацца на перакрыццях фронтэнду
Шырока адвалгатасць заключаецца у тым, што форма рэгістрацыі ўжо пазначае поля як required і пераканальвае формат электронной пашты ў браузеры. Аднак гэтага недастаткова, адколі з точкі зору нападчыка фронтэнд ёсць необавязковым. Кожны можа безпосередні за дапамою Postman, curl або скрыпты выклікаць ваш API і айсці за ўсіма правіламі, якія є у вашай інтэрфейсной сістэме. Якщо сервер сам не пераканальвае даныя, ён фактычна давае доверлівасць тым, хто надсылае запит.
Конкрэтныя прынцыповыя перавагі верыфікацыі з боку сервера:
- Гэта захоўвае вашы даныя. Калі столбець апэкаваеся цячынай, але прымае
"twenty-five", вы або атрымліваете памылку, або, што горша, некоректны запис, які пазней вызывае багі.
Разпад працы ўсё прост: верыфікацыя на фронтэндзе служыць для дапамогі корыстнікам, а верыфікацыя на бэкэндзе — для захавання прыемлівасці. Обеяяя ўжоць ценныя, але толькі бэкэндзавой слой ёсць обавязковым.
Верыфікацыя вручную і прычыны, чаму гэта спакоюе масштабаванне
Цікава будзе спачатку паглядзець на ручную версію. Цэў тут пачатак маршруту рэгістрацыі, які запоўнюе полья з распакаванага тэлу (гэта предпакладае, што зарегистраваны парсер тэлу у формате JSON, такі як express.json()):
app.post("/register", (req, res) => {
const { name, email, age, password } = req.body;
Кожнае полья потым атрымвае свой сабэйшны клазус, які вяртае адпаведную адпаведнасць 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" });
});
Для маленькага прыемлівасця гэта абсалютна прийнятна. Аднак, калі кодавая база расте, проблемы накапліваюцца:
- Кожны маршрут, які прыемлівае данні, патрэбуе свой блок оператаў
if. - Перакананні копіююцца межа маршрутамі, і паследнія змены, апранутыя да адной копіі, лёгка працягваюцца не з’яўляцца ў іншых.
null, неправильныя типы і значэння, якія занадта короткія або занадто дзьвянкія, быстра стае вельмі клопачлівай.У ручной версіі таксама є тонкія недагодзеныя. email.includes("@") прыме багато некоректных адресаў, а вызов .includes для нестроковага значэння выклікае памылку. Такая повтаральная, спрыяючая памылкам пераканальвання — гэта самэ тое, што павінна адрабатваць бібліятэка.
Што дае express-validator
express-validator — это калекцыя мідлвэраў Express, створаных на адной падставе з бібліятэкай validator.js. Уместо таго, каб пісаць умовную логіку для кожнага поля, вы прызначаеце правілы, а бібліятэка пераканальвае данні запыту па ўсім іхнім списку. Пачніце з імпорту двух галоўных функцый:
const { body, validationResult } = require("express-validator");
Той самы рут рэгістрацыі, перапісаны з дэкларатывнымі правіламі, выглядае так:
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" });
}
);
Правілы тепер сформульаваны майже простаю мовай: імя пасля вычысцэння слоў не можа быць порожнім, адказ прызначаны для электронной пошты, вік павінен быць цэлым числам не менее 18, а пароль — не менее восьмі симвалам. Функцыя-обраОбавальнік у канцы развязвае толькі адна задача: чы не было жадных проблем?
Што адбываецца, калі прыходзіць запит
Найпаўзначыцэйнейшая памылка новачакоў — гадкі, што express-validator апоштова адхіляе некоректныя запиты. Гэта не так. Ён збірае інфармацыю пра тое, што не так, і залічвае адпаведны адказ усунуць вам. Прыемна для кожнага запита выглядае так:
- Express запускае ланцуг мідлвэра, заданы для рута, і выконвае кожны элемент у порядку.
body("email").isEmail(), ён чытае значэнне email з req.body і пераканальвае яго. Пры невыпаленні атрыбутацыяй не выклікаецца і не завершваецца запит; ланцуг фіксуеюць адказку на об’екте запита і працюе далей.validationResult(req) чытае фіксаваныя адказкі. Цэта звычайная функцыя, а не мідлвэр, таму яе вызываюць усередзіні обробніка запита. Яна сама нічога не пераканальвае; яна проста адносіць тое, што вже скарысталі ланцугі.errors.isEmpty() вяртае false, вы адправляеце код 400 з деталямі. Інакшыя вы працюеце над справжнім завданням.Дыяграма прадаё прывілею гнучкасці над зручнасцю. Пакалькі адхоўленне не ёст эўтаматычным, вы кантролюеце формат і час адпаведзі, а таксама можаце выконваць іншую логіку между пераканальваннямі і заканчоўным рашэннем. Адсутнасць пераканальвання validationResult() тыхо выключае пераканальванне для таго маршруту, і самэ гэта прычына, чаму багато команд абгортваюць пераканальванне ў маленькій повторна выкарыстоўванай проміжнай бібліятэцы.
Чатыры базовыя складовы
Калі вы ведаеце этыя элементы, бібліятэка становіцца простай у викорыстоўванні.
Лянцюгі пераканальвання
Функцыі такія як body(), query(), param() і check() ствараюць ланцуг: серыё правілаў, з’еднаных крапкамі, напрыклад body("email").trim().isEmail(). Кожна з іх працюе з разным часткам запиту:
body()чытаеreq.body— даныя у формате JSON або паданне формы.query()чытаеreq.query— строку запита пасля знака?, напрыклад?page=2.param()чытаеreq.params— параметры маршруту, такія як:idу адресе/users/:id.check()аналізуе ўсі часткі запиту адразу.
Автаперакрыцчыкі
Валідатары — это функцыі, якія пераканаліваюць значэнне: isEmail(), isInt(), isLength(), isAlphanumeric() і ўсё іншае. Большасць з іх апілююцца да validator.js, які ўжо мае розвітаную, добра працавану логіку для распашчытых форматоў.
Санітайзеры
Санітайзеры ператвараюць данні, а не проста яны пераканаліваюць. trim() адзяўляе прылеглыя прабелы, normalizeEmail() ператварае адресу у стандартную форму, а escape() заменяе символы, значныя для HTML, на ўсоўнікі гэтых симвалаў, каб іх не можна было спрыяцельваць як маркап.
Хранэнне рэзультатаў за кожны запыт
Калі працюе ланцюг, яго неякосці прыязджаюць да таго конкрэтнага об’екта req. Функцыя validationResult(req) проста чытае данні з гэтаго сховішча, таму ёй патрэбен той самы об’ект запита, які быў адпрацаваны ланцюгамі.
У сукупнасці прабегае так: прыходзіць запит, ланцюгі працуюць і тыха зафіксаваюць проблемы, обробнік запита выкарыстоўвае гэтыя рынуткі, а вы выбіраеце адпаведную адпаведь. Якщо вы параганяеце гэты падход з верыфікацыей на адной основе схемы, наша парабярэнне Zod і express-validator рассматрывае ўсі плюсы і мінусы.
Методы, якія падходзяць для большасці рэальных проектаў
Вам не патрэбна вся API. Невялікі набор методаў падходзіць для большасці случаяў.
Селектары месца
body(field)— для поль у тэле запитаquery(field)— для параметраў у рэчы запита
param(field) для параметраў маршрутуЗвычайныя верыфікаторы
.notEmpty(): значэнне не можа быць пустым.isEmail(): значэнне павінна быць правдападобным адресам электронной пошты.isInt({ min, max }): значэнне павінна быць цэлым числам, за неабяжным выключэнням — у заданых межах.isLength({ min, max }): пераканальвае дыяпазон дужоў строкі.isString(): значэнне павінна быць стракой.isBoolean(): значэнне павінна быць логічным значэннем.matches(regex): значэнне павінна адпавядаць спецыяльнаму шаблону.isIn([...values]): значэнне павінна быць адным з фіксованага набору, што падходзіць для такіх полей, якstatus, які можа быць або"active", або"inactive"
Звычайныя санітайзеры
.trim(): адзьякуе паўністы прабелы на пачатку і ў канцы.escape(): экрануе симвалы HTML, каб зменшыць рызык інжэкцый.normalizeEmail(): стандартазуе формат адказаў на падпіс.toInt()і.toFloat(): ператварае строковы вхід у цалкі
Свае правілы
.custom((value, { req }) => { ... }): рэалізуе вашае сабскладаное правіло, калі жадны з вбудованых верыфікатораў не падходзіць, напрыклад, для пераканання, што адны ўжо не зарэўнаваны
Адчыт пра бяды
.withMessage("text"): задае паведамленне для паказвання пасля пыткі правіломvalidationResult(req): скупляе всі бяды для текущага запитуerrors.array(): ператварае гэтыя бяды у звычны масіў, падходзячы для адказу у формате JSON
Маршрут у стылі продакшну
Наступны прыклад бліжэй да таго, што мае рэальны API: маршрут, які створае пастку блогу, аднаражваючы перакананне, санітараванне і перагляд базы дадзеных. Спачатку — імпорты, дзе Post паводлівае модэлю, такой як схема Mongoose:
const express = require("express");
const { body, validationResult } = require("express-validator");
const Post = require("./models/Post"); // a Mongoose model, for example
Потым — рутэр і маршрут з яго массивам правілаў:
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;
Є калькі деталяў, якія варта выявіць:
- Правілы, апараныя на базе дадзеных. Ланцоўка
slugвыкорыстоўвае асінхронны валідатор.custom(), каб пераглянуць існуючыя пасткі. Регулярны выраз можа паказаць формат slug, але толькі запыт можа паказаць, чы ён унікальны. Выклекванне пад час выконання функцыі custom пазначае поле як неваліднае з такым прыемам.
tags пазначаецца як .optional(), таму пастка без атрыбутаў tags прыймаецца, але калі атрыбуты tags задаюцца, яны павінны быць масоўкай страк.Два застережэння пры копіюванні гэтага шаблона. Першае — пераконтроль на унікальнасць у валідаторы можа стаць прычынай конфлікта: два запиты з адным і тым же слагам могу оба працаваць правільна перад тым, як хоць адны з іх будзе зафіксаваны, таму трэба падтрымваць унікальны індэкс у столбцы і таксама караць бягу пра адміністрацыю памылкі дублюючага ключа. Другае — перадача req.body безпосередна да Post.create() зафіксавае ўсі дадатковыя поля, якія включыў кліент, а не толькі тые, якія былі перакантролены. express-validator апранае метод matchedData(req), які вяртае толькі тые поля, якія падпадаюць пад вашы ланцугі перакантроўкі, і ўжыце яго дапамагае усунуць гэты прынт распадзелу данных.
Калі аплікацыя расте, трэба пераносіць масывы правіл у спецыяльныя модулі, напрыклад validators/postValidator.js, і імпортуваць іх у файлы маршрутаў, каб маршруты не ператворыліся на масы коду перакантроўкі.
Заключэнне
Параболіка няўт прыемлівага дадатковага элемента; яна вярнайшае, чы можа ваша прыкладка даверляць да дадзеных, якія яна обрабоўвае. Рукамі напісаныя перагледы з if падходзяць для калькі полей, але быстра ствараюць проблемы ў большай колькасці маршрутаў.
express-validator — гэта не чары. Ён выканае заявленыя вам правіла як мідлвэр, фіксуе неудачы ў запитах і дазволяе validationResult() даць вам атлас, каб вы моглі выбраць адпаведную адпаведзь. Усвядомляючы гэты прыем, рэшта бібліятекі ўтварае набор параболікаў і санітайзероў, якія можна выбраць за патрэбай. Найважлівейшыя моманты ў практыцы:
- Спрыяйце параболіцы на бэкенде як обав’язковай, незалежна ад перагледаў на фронтэнде.
- Заўжды вызывайце
validationResult(), жадаўна з аднаго спяльванага мідлвэра, таму што бібліятека ніколі сама не адхіляе запыт. - Усвядомлена расставляйце санітайзеры ў ланцугу.
matchedData(req), а не самы чысты тэкст пакета.