Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як перевірити дані запиту в Express.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Валідація запиту в Express.js** - це перевірка `req.body`, `req.params` і `req.query` за схемою до запуску будь-якої бізнес-логіки. Для TypeScript проектів підходить Zod, для plain JS - Joi. ```js const validate = (schema) => (req, res, next) => { const result = schema.safeParse(req.body); if (!result.success) return res.status(400).json({ errors: result.error.errors }); req.body = result.data; next(); }; app.post('/users', validate(userSchema), createUser); ``` **Ключове:** валідація завжди в middleware, до async-операцій.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Валідація запитів в Express.js** - це перевірка вхідних даних за заданими правилами до того, як вони потраплять до обробника маршруту. ## Теорія ### TL;DR - Схема - це список гостей, `req.body` - це людина на вході. Не відповідає правилам - не проходить. - Головний підхід: описуєш схему (Zod, Joi або express-validator), обертаєш її в middleware, підключаєш до маршруту. - TypeScript проект? Zod - він виводить типи автоматично. Звичайний JS? Joi. Треба швидко перевірити пару параметрів? express-validator. - Валідація йде до будь-якої async операції. Перевіряти дані після запиту до БД - марна трата ресурсів. - Всі три бібліотеки повертають структуровані помилки з деталями по кожному полю. ### Швидкий приклад ```js const { z } = require('zod'); const express = require('express'); const app = express(); app.use(express.json()); const userSchema = z.object({ name: z.string().min(2), email: z.string().email() }); const validate = (schema) => (req, res, next) => { try { schema.parse(req.body); next(); } catch (err) { res.status(400).json({ errors: err.errors }); } }; app.post('/users', validate(userSchema), (req, res) => { res.json({ message: 'User created', data: req.body }); }); // { "name": "Bob", "email": "bob@example.com" } -> 200 OK // { "name": "A", "email": "invalid" } -> 400 з помилками по полях ``` `validate()` - функція вищого порядку, що повертає middleware. Схема перевіряється до обробника. Якщо перевірка не пройшла - запит зупиняється тут, обробник маршруту не запускається. ### Схеми проти ручних перевірок Ручні `if` підходять для одного-двох полів. Далі вони розпадаються: ті самі перевірки копіюються по маршрутах, граничні випадки з приведенням типів упускаються, з'являються прогалини в безпеці. Схемні бібліотеки централізують правила, автоматично приводять типи і повертають структуровані повідомлення про помилки, які можна відправити клієнту без додаткової обробки. Zod до цього виводить TypeScript-типи з схеми, тому `req.body` стає повністю типізованим нижче по ланцюжку без окремих інтерфейсів. ### Яку бібліотеку обирати - **Zod**: TypeScript проект, нова кодова база, потрібні виведені типи без дублювання. - **Joi**: звичайний JavaScript, legacy-додаток або команда вже знайома з екосистемою Hapi. Трохи швидший за Zod при великому навантаженні. - **express-validator**: мінімальне налаштування, треба перевірити кілька URL-параметрів чи query-рядків без повноцінної схемної бібліотеки. - **class-validator**: NestJS або архітектура на основі класів, потрібен декораторний синтаксис на DTO. ### Порівняння бібліотек | Бібліотека | Підтримка TS | Формат помилок | Продуктивність | Коли використовувати | |---|---|---|---|---| | **Zod** | Нативна (виведені типи) | Детальні, з шляхом до поля | Висока | Нові TS проекти | | **Joi** | Ручні типи | Детальні | Найвища | Plain JS / legacy | | **express-validator** | Відсутня | Базові | Мінімальна залежність | Швидкі перевірки параметрів | | **class-validator** | Декоратори | Налаштовувані | Повільніша (рефлексія) | NestJS / класи | ### Як це працює всередині `express.json()` розбирає тіло HTTP-запиту в `req.body` через Node.js `http.IncomingMessage`. Далі в стеку запускається validation middleware. Zod обходить об'єкт рекурсивно, перевіряє типи і обмеження по черзі. Якщо щось не збігається - кидає `ZodError`. Middleware перехоплює помилку і відповідає 400, обробник маршруту не запускається взагалі. Одна деталь, яка постійно дивує: значення `req.query` завжди рядки. Запит `?page=2` дає рядок `"2"`, а не число `2`. Zod вирішує це через `.transform(Number)`, але додавати треба явно - автоматично не відбувається. ### Типові помилки **Помилка 1: валідація всередині обробника, після async-операцій.** ```js // Неправильно: запит до БД виконується навіть при невалідних даних app.post('/users', async (req, res) => { await db.connect(); schema.parse(req.body); // запізно }); // Правильно: middleware запускається першим app.post('/users', validate(schema), async (req, res) => { await db.connect(); }); ``` **Помилка 2: забути `express.json()`.** Без нього `req.body` буде `undefined`, і `schema.parse(undefined)` падає зі заплутаною помилкою замість чистого 400. Додавай перед маршрутами, одразу з обмеженням розміру: ```js app.use(express.json({ limit: '10kb' })); ``` **Помилка 3: використовувати `.optional()` на обов'язкових полях.** ```js // Неправильно: пропускає порожні рядки та null name: z.string().optional() // Правильно: непорожній рядок name: z.string().min(1) ``` Це часте джерело помилок обмежень БД, які з'являються значно пізніше в ланцюжку запиту - далеко від місця реального баґу. **Помилка 4: не трансформувати числа з query-рядка.** ```js // Неправильно: "abc" проходить z.string() але ламає логіку далі query: z.object({ page: z.string() }) // Правильно: трансформація і перевірка числа query: z.object({ page: z.string().transform(Number).refine(n => !isNaN(n) && n > 0, 'Має бути позитивним числом') }) ``` ### Де використовується - **Next.js API routes**: Zod на `/api/users` з виведеними типами обробника, стандарт у шаблонах Vercel. - **NestJS**: class-validator декоратори на DTO, стандарт у більшості enterprise-проектів. - **Strapi CMS**: Joi-схеми для кастомних ендпоінтів плагінів. - **Звичайні Express API**: express-validator для швидких перевірок параметрів, Zod для складного тіла запиту. ### Питання на співбесіді **Q:** Чому middleware краще за валідацію всередині обробника? **A:** Middleware запускається до будь-якої бізнес-логіки, не витрачає ресурси на невалідні запити і дозволяє використовувати ту саму схему на кількох маршрутах. Добре компонується з auth-middleware в одному ланцюжку. **Q:** Яка різниця між `parse()` і `safeParse()` в Zod? **A:** `parse()` кидає `ZodError` при помилці - використовуй в try/catch всередині middleware. `safeParse()` повертає `{ success: boolean, data | error }` і ніколи не кидає виняток - зручно для WebSocket-обробників або не-HTTP контекстів. **Q:** Як валідувати завантаження файлів? **A:** Спочатку розбираєш multipart-тіло через Multer, потім валідуєш `req.body` і `req.file` через Zod. Тип файлу перевіряй через `z.enum(['image/jpeg', 'image/png'])` на `req.file.mimetype` після того, як Multer відпрацював. **Q:** Чи захищає Zod від prototype pollution? **A:** Так, з версії 3.22 ключі `__proto__` та подібні видаляються автоматично. Joi теж видаляє невідомі поля за замовчуванням. Якщо треба відхиляти будь-які непередбачені поля замість видалення - додавай `.strict()`. **Q (senior):** Ти будуєш API gateway для мікросервісів з 50+ схемами, що завантажуються динамічно по маршруту. Як проектуєш шар валідації? **A:** Тримаєш центральний реєстр схем (`Map` in-process або Redis для розподілених систем) з ключем по route ID. Завантажуєш схеми ліниво і кешуєш скомпільовані Zod-об'єкти. Генератор OpenAPI-to-Zod тримає схеми в синхроні з API-контрактами. Для не-TS сервісів - Joi як запасний варіант. Версіонування через ключ реєстру: `users:v1` і `users:v2` співіснують без конфліктів. ## Приклади ### Базовий: валідація тіла запиту через safeParse ```js const { z } = require('zod'); const express = require('express'); const app = express(); app.use(express.json()); const loginSchema = z.object({ email: z.string().email(), password: z.string().min(8) }); const validate = (schema) => (req, res, next) => { const result = schema.safeParse(req.body); if (!result.success) { return res.status(400).json({ errors: result.error.errors }); } req.body = result.data; // використовуємо провалідовані, приведені дані next(); }; app.post('/login', validate(loginSchema), (req, res) => { res.json({ message: 'Credentials valid' }); }); // { email: "a@b.com", password: "secret123" } -> 200 // { email: "notanemail", password: "short" } -> 400 з двома помилками по полях ``` `safeParse()` зручніший за `parse()` у middleware - він ніколи не кидає виняток, помилка це просто значення, яке перевіряєш. Присвоєння `result.data` назад до `req.body` гарантує, що обробник отримає приведені дані, а не сирий ввід. ### Середній рівень: тіло і query-параметри разом ```js const createUserSchema = z.object({ body: z.object({ name: z.string().min(2).max(50), email: z.string().email(), password: z.string().min(8).regex( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Пароль повинен містити великі, малі літери та цифру' ) }), query: z.object({ referral: z.string().optional() }) }); const validate = (schema) => (req, res, next) => { const result = schema.safeParse({ body: req.body, query: req.query }); if (!result.success) { return res.status(400).json({ error: 'Validation failed', details: result.error.errors.map(e => ({ field: e.path.join('.'), message: e.message })) }); } next(); }; app.post('/users', validate(createUserSchema), async (req, res) => { res.json({ userId: 123 }); }); // POST /users?referral=abc { name:"Alice", email:"a@b.com", password:"Pass123" } -> 200 // POST /users { name:"A", email:"bad", password:"weak" } -> 400 з трьома помилками ``` Одна схема охоплює і `body`, і `query` - один middleware-виклик на маршрут. Кожна помилка у відповіді містить повний шлях до поля, наприклад `body.password`, тому клієнт одразу розуміє що пішло не так. ### Просунутий рівень: вкладені масиви з трансформацією Цей шаблон часто збиває з пантелику - `.transform()` в Zod змінює форму виводу, і `req.body` в обробнику виглядає інакше ніж вхідні дані. ```js const orderSchema = z.object({ items: z.array(z.object({ productId: z.string().uuid(), quantity: z.number().int().positive().max(99), metadata: z.record(z.any()).optional() // дозволяє довільні поля без помилки })) .min(1) .max(10) }).transform(data => ({ ...data, totalItems: data.items.reduce((sum, item) => sum + item.quantity, 0) })); const validate = (schema) => (req, res, next) => { const result = schema.safeParse(req.body); if (!result.success) { return res.status(400).json({ errors: result.error.errors }); } req.body = result.data; // містить totalItems, обчислений Zod next(); }; app.post('/orders', validate(orderSchema), (req, res) => { console.log(req.body.totalItems); // вже обчислено, не треба рахувати знову res.json({ order: req.body }); }); // { items: [{ productId: "550e8400-e29b-41d4-a716-446655440001", quantity: 5 }] } // -> 200, totalItems: 5 // { items: [] } // -> 400 "Array must contain at least 1 item(s)" // { items: [{ productId: "not-uuid", quantity: 0 }] } // -> 400 дві окремі помилки ``` `.transform()` запускається лише після того, як усі перевірки пройшли. В `req.body` потрапляє чистий, збагачений об'єкт - рахувати `totalItems` в обробнику більше не треба. Zod 3.22+ також видаляє `__proto__` та подібні ключі автоматично, тому захист від prototype pollution вже вбудований.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.