Адміністрацыя межы Express: One Zod Middleware для тэлу, параметраў і запитаў
Дазвольце дазнаць, як прабавіцца пераканаць цэлыя тэлы запыткаў Express, параметры маршрутаў і строкі запыткаў за дапамогай аднаго віднаходжлівага мідлвэра Zod, а таксама як ён дапамагае у пераканаці модэляў Sequelize.
Нічыта не заважае кліенту падаць номер там, дзе ваш API чакае імя, або null там, дзе чакае пароль. Код, які слепа даверыць req.body, зрэшты запісвае неякосці або выклікае памылкі, якія не стосуюцца ўсёй справы. У гэтым кяліку показана, як аднойчы за дапамогою Zod апісаць правільны вхідны дадзеныя, прымусова ўжываць яго ў аднам сервісе Express, які карыстаецца тэлам дадзеных, параметрамі маршрута і строкай запыткі, і дазволіць контралерам сфокусавацца на бізнес-логіцы.
Проблема: запыткі прыходзяць без паведамленнях типаў
Ёсць абсалютна законны HTTP-пэйлоад, які ніякий канцэнтр па рэгістрацыі не должен прымаць:
{
"fullName": 123,
"email": "hello",
"password": null
}
Кожна полька мае неправільную структуру. Zod — это бібліятэка шыматоў для JavaScript і TypeScript, якая дазволяе точна задаць тое, чаго вы чакаеце, і атрымаць або чыстыя дадзеныя, або структураваны список проблем.
Апісаванне вхідных дадзеных як шымата
Дзякуючы, рэгістрацыя трэбуе строку fullName, правільна адреса email, password дужоўжынаю не менш чым восем характароў, а таксама необавязковы цэлычасны age. У Zod гэта выглядае практычна як самыя трэбаванні:
const { z } = require('zod');
const registerSchema = z.object({
fullName: z.string().min(2),
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().min(18).optional()
});
Правілы знаходзяцца ў аднам об’екте, а не ў розрозных оператывах if. Правіло ўзгляду таксама выкананае на абавязковы возраст 18: якшо возраст не заданы, тое працюе, а якшо 16 — не працюе.
Інсталляцыя і імпорт
Zod — это звычная залежнасц npm:
npm install zod
У CommonJS імпортуеце пространство імен z за дапамойкай require:
const { z } = require('zod');
У ES modules выкарыстоўваеце імпорт па імені:
import { z } from 'zod';
Дадзенне чытабельных паведамленняяпарабоек
Każdy верыфікатор прыймае необавязковае паведамленне, якое будзе адображанаць кліентам:
const registerSchema = z.object({
fullName: z.string().min(2, 'Full name is required'),
email: z.string().email('Invalid email'),
password: z
.string()
.min(8, 'Password must be at least 8 characters'),
age: z
.number()
.int()
.min(18)
.optional()
});
Падлой, яка задовольняе всі правіла, працюе без змян:
{
"fullName": "John Smith",
"email": "john@example.com",
"password": "password123",
"age": 25
}
У гэтым прыкладзе імя занадта корача, адреса не мае домэна, а пароль складаецца з трохі симвалаў:
{
"fullName": "J",
"email": "invalid-email",
"password": "123"
}
Zod адзначае ўсі тры проблемы заодно, таму форма можа выявіць усе некоректныя поля за адну пераправку дадзеных.
Няўясныя версіі Zod (v4 і новейшыя) таксама аддаюць верыфікаторы вышэйшага рангу, такія як z.email(), і заменяюць стары спосаб z.string().email(). Спосаб з ланцоўкамі яшчэ працуе, але краща пераканацца ў дакументах да вашай версіі.
Выбор между parse() і safeParse()
parse() выклікае аберанне
parse() вяртаў пераверыраныя дадзеныя або выклікае ZodError:
const data = registerSchema.parse(req.body);
У обробніку Express трэба самостоятельна падхапіць аберанне або перасłaць яго за дапамогою next(err).
safeParse() вяртае рэзультат
safeParse() ніколі не выклекчвае асоцыяцый. Ён вяртае об’ект з флагам success, які лепей падходзіць для обробкі запыткаў, таму што некоректны вхідны дадзеныя є стандартным рэзультатам, а не асоцыяцыйным:
const result = registerSchema.safeParse(req.body);
У разе неудачы error.issues пераказвае кожную проблему з ўсімі деталямі і паведамленням, гатовую для адпаведнай адпаведзі 400:
if (!result.success) {
return res.status(400).json({
success: false,
errors: result.error.issues
});
}
У разе успеху result.data зберагае аналізаваныя значэння:
const data = result.data;
З даўэйшых часоў выкорыстоўваеце result.data, а не req.body: невядомыя ключы за замовчаннем адсікаюцца, а таксама вже застосаваныя ператварэнняі і значэнні-заместнікі.
З аддатковых пераканальнікаў да відновліваемага мідлвэра
Спакойнейшая форма інтэграцыі — выклік safeParse() унутрь обрабоўчыка:
app.post('/register', (req, res) => {
const result = registerSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: result.error.issues
});
}
const data = result.data;
console.log(data);
// Continue with registration logic...
return res.status(201).json({
success: true,
data
});
});
Це працюе, але калі є 20 чырвоны 50 канцэнтраў, тыя ж рэчы падклеююцца ў кожны контролер, і паступова ствараецца плутанне. Таксама зазначыце, што ў гэтым прыкладзе апрацаваны об’ект, уключаючы пароль, вяртаецца назад да кліента; справжній канцэнтраў должен вяртаць толькі несупыльныя поля.
Фабрыка validate()
Фабрыка, паказаная нижэй, прымея шымат і вяртае экспрэс-хэндлер. Яна апрацоўвае тэла запыту, параметры і умовныя значэння разам, у случае аберання вяртае код 400, а інакшы зберагае апрацаваны рэзультат у req.validated прычым вызываючы next():
const validate = (schema) => {
return (req, res, next) => {
const result = schema.safeParse({
body: req.body,
params: req.params,
query: req.query
});
if (!result.success) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: result.error.issues
});
}
req.validated = result.data;
next();
};
};
module.exports = validate;
Важны два моменты. Запіс у окремую властивасць req.validated запобігае проблемам у Express 5, дзе req.query ўжо є гетером і не можа быць проста перепрацаваны. А так как мідлвэр пакуе вхідныя даны як { body, params, query }, шаблонам неабходна будаць адпавядаць гэтай структуры. Плоскі registerSchema будзе шукати fullName на верхнім роўні і адхіляць кожны запит, таму яго трэба пакаваць як z.object({ body: registerSchema }) або ж змусіць мідлвэр пераверываць толькі req.body.
Праўэльне падключэння да маршруту
Мідлвэр знаходзіцца межы маршруту і кантролера:
router.post(
'/register',
validate(registerSchema),
register
);
Пайплайн запытоў стае такім:
Request
↓
Express Router
↓
Zod Validation Middleware
↓
Controller
↓
Service
↓
Database
Невялікі вхідныя даны зупіняюцца на мідлвэре, і кантролер ніколи не запускаецца; вялікі даны продовжуюць свой шлях, пры чым гарантуецца, што яны адпавядаюць шаблону.
Зберагчык кантролераў пра логіку бізнесу
Без шара адрабоўкі, кантролер акумулюе ўсе прыбуткі адразу:
const register = async (req, res) => {
// validation
// check email
// validate password
// validate name
// business logic
// database operation
};
З наявнасцю мідлвэра ён проста чытае перакананыя значэння:
const register = async (req, res) => {
const {
fullName,
email,
password
} = req.validated.body;
// Business logic
};
Як бонус, схемы можна тэставаць як елементарныя об’екты, і тэсты кантролераў больш не патрабуюць прыкладаў для кожнага некоректнага пакета дадзеных.
Адрабоўка параметраў маршрута з прымусам пераканання
Той самы падход працюе і з сегментамі URL. Возьмемо запит на адного пользователя:
GET /users/123
Схема для параметра id:
const userParamsSchema = z.object({
id: z.coerce.number().int().positive()
});
Прыўязана як ранейш (пад ключам params калі викорыстоўваецца з вышэйзгаданым мідлвэрам):
router.get(
'/users/:id',
validate(userParamsSchema),
getUser
);
Ключовы элемент — прымус пераканання:
z.coerce.number()
Усё ў URL — это текст. Значэнне
req.params.id
прыбывае як строка
"123"
а не як число
123
Звычны z.number() адзін раз адхрасіў бы кожны запит. z.coerce.number() спачатку працоўвае з вхідным дадзеннем через Number(), а пасля прыкладзе .int() і .positive(). Є адны крайняй случай: Number('') дае 0, таму порожняя значэнне стае нулем. Тут .positive() яго фіксуе, але схема без ніжней межы дазволіла б яму пройсці.
Паўнерчэнне строк запыту з стандартнымі значэннямі
Пагінацыя — это класычны прыклад строки запыту:
GET /users?page=1&limit=10
Прыведэнне да стандарту паўсарбаванае зі стандартнымі значэннямі дазволяе атрымаць безпечныя числы нават тады, калі кліент іх не прадаў:
const userQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(10)
});
Ліміт .max(100) таксама не дазволяе кліенту запрашваць мільйон рэкордаў за адны запыт.
Тыповыя элементы пабудовы Zod
Большасць схем складаецца з невялікага набору элементаў:
z.string(),z.number(),z.boolean()пераканаліваюць прымітывыя типы.z.object()описвае структуру об’екта;z.array()пераканалівае масэв і яго элементы.z.enum()обмежвае значэнне фіксаваным списаком вароў..min()і.max()абмежваюць значэнне числа чыста дыяўерсуі строкі чыста дыяўерсуюць дужыну масэва..email()пераканалівае формат электроннай пашты;.int()выкарыстоўваецца для цэлых чисоў;.positive()— для значэнняў, большых за ноль..optional()дазваляе абсэнцыя польа;.nullable()дазваляе явныnull;.default()запэўняе відсутныя значэнні.z.coerce— это прастор імен, а не функцыя:z.coerce.number()і падобныя функцыі пераканаліваюць вхідны дадзеныя пры пераканаленні.
.refine() дадзеяе спецыяльныя правілы; .transform() перакаштоввае значэнне пасля таго, як яно праходзіць..parse() выклекае адзінакоўнае застосованне пад час адмовы; .safeParse() вяртае рэзультат успеху або адмовы.Прыклад: запись пользователя з ролямі
Пользователь у дапытчыку для керування дэцячым садком можа выглядаць так:
const userSchema = z.object({
fullName: z.string().min(2),
email: z.string().email(),
role: z.enum([
'admin',
'teacher',
'parent'
]),
isActive: z.boolean().default(true)
});
z.enum() адхоўчае будзь-якія іншыя ролі, а isActive па значэнню за замовчанням ёсць true, калі яго не задаўце. Шымат таксонаміі служыць таксама як дакументацыя.
Zod і Sequelize атстояць разныя слоі
Команды, якія работаюць з Sequelize і MySQL, часта спытваюцца, чаму ўсё-такі патрэбны Zod, калі моделі вже маюць верыфікаторы. Ція два інструмента захоўваюць разныя межы.
Zod атстоіць межу API
Яна пераканальвае, што прыходзіць праз HTTP, прычаму код аплікацыі ўжо не можа з яго паводзіцца:
HTTP Request
↓
Zod
↓
Controller
Sequelize атстоіць слой дадзеных
Їхнія функцыі параболкі запускаюцца калі зберагаецца модэль, глыбока ў шары служб:
Controller
↓
Service
↓
Sequelize
↓
MySQL
Іспытанне обох
Разам яны ствараюць два незалежных шары:
Client
↓
Express
↓
Zod
↓
Controller
↓
Service
↓
Sequelize
↓
MySQL
Zod дае быстрыя, зручныя для кліента адпаведзі 400; Sequelize ловіць памылкі, якія выходзяць з самай працэй аплікацыі, напрыклад, задачу на фоне, якая стварае некоректны запис. Абмежэнні базы дадзеных, такія як NOT NULL і унікальныя індэксы, застаюцца фінальным захамоўкам.
Арганізацыя схэм у большай базе коду
У проекте на адміністрацыі модуляў кожны модуль мае файл параболкі рядом з яго маршрутамі, кантролерам і службай, а спяльны мідлвэр знаходзіцца ў сваёй сабе папке:
src/
├── modules/
│ └── users/
│ ├── user.controller.js
│ ├── user.service.js
│ ├── user.routes.js
│ └── user.validation.js
│
├── middleware/
│ └── validate.js
│
└── app.js
user.validation.js экспортуе схэмы модуля:
const { z } = require('zod');
const createUserSchema = z.object({
fullName: z.string().min(2),
email: z.string().email(),
password: z.string().min(8)
});
module.exports = {
createUserSchema
};
а файл з маршрутамі застаецца короткім:
router.post(
'/users',
validate(createUserSchema),
createUser
);
Калі змінюецца поле, кантролер і яго правілы рэдагуюцца разам. Чырвоны тыя ж схемы ў браузеры, адгледзіце як выкарыстоўваць адну схему Zod у React і Node.
Чаму ўніверсальны источнік правды ёсць корыстны
Без схемы верыфікацыя прасачываецца ў кантролеры у виглядзе спецыяльных пераканальняў:
if (!email) {
// ...
}
if (!password) {
// ...
}
if (password.length < 8) {
// ...
}
if (!['admin', 'teacher'].includes(role)) {
// ...
}
Кожны канцэнтрык павторае нейкі адносна іншы варыянт, і ніхто не можа за мгновеннае адгледзенне пабачыць всю умову. Адпаведная схема выказвае яе за калькі ліній:
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
role: z.enum(['admin', 'teacher'])
});
Гэтае ўгода межу API і кліентамі, якая выканана ў аднам месцы. Для паўтарэння з іншым популярным падходам, адгледзіце Zod проты express-validator.
Ключовыя выводы
Праўяценая цэннась — это парадакт абавязкаў, якія прыменяе Zod:
Request
↓
Validation
↓
Controller
↓
Business Logic
↓
Database
- Атрыбуцыяйце данні на входзе за дапамою
safeParse()і дазвольце толькіresult.dataпрабрацавацца працоўнікамі обробкі. - Центрырацыяйце атрыбуцыяйцюванне ў аднам мідлвэры, і прабачце кожны схема, каб яна адпавядала формату, які ён парасэ.
- Ўжывайце
z.coerceдля параметраў і стрэнгаў запытанняў, а таксама лімітуйце значэнні, такія як размах староніцы. - Залічвайце атрыбуцыяйцювальнікі ORM і обмежэнні базы дадзеных як другі слой, а не як замену ім.
- Размешчайце схемы разам з іх модулямі, каб змяны контракту супаўставляліся з кодам.
Супаўязаныя матэрыялы
- Zod vs express-validator: Два падчынства апрацоўкі верыфікацыі у Express — Поручае верыфікацыю запытак на адной схеме з Zod з відпаведным мідлвэрам express-validator, які работае на асоцыяцыйных ланцутках; расглядае настройкі, форматаванне адзінакоў і частыя проблемы.
- Адна схема Zod для React-фронтэнда і Node-бэкэнда — Дзеўяце, як адна схема Zod можа верыфікацыяваць формы ў React, адпаведныя адказы API, тэлы запытак у Express і зменныя сераўіса, а таксама ствараць відпаведныя типы ў TypeScript.