Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює парсинг тіла в Express.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Парсинг тіла (body parsing) в Express.js** перетворює сирий потік байтів HTTP-запиту на JavaScript-об'єкт у `req.body`. Додай `express.json()` для JSON API та `express.urlencoded({ extended: true })` для HTML-форм, обов'язково перед маршрутами. ```js app.use(express.json()); app.post('/users', (req, res) => { console.log(req.body); // { name: 'Alice' } }); ``` **Головне:** без парсера `req.body` завжди `undefined`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Парсинг тіла (body parsing) в Express.js** читає сирий потік байтів з HTTP-запиту і перетворює його на JavaScript-об'єкт у `req.body`, спираючись на заголовок `Content-Type`. ## Теорія ### Коротко - Без парсера `req.body` завжди `undefined`, навіть якщо клієнт надіслав дані - `express.json()` для `application/json`; `express.urlencoded()` для HTML-форм - Реєструй парсери до маршрутів, не після - потік читається лише раз - Ліміт за замовчуванням 1mb; змінюй через `{ limit: '10kb' }` на рівні маршруту - `multipart/form-data` (завантаження файлів) потребує `multer`, не вбудованих парсерів ### Базовий приклад ```js const express = require('express'); const app = express(); // Парсери ДО маршрутів app.use(express.json()); // application/json app.use(express.urlencoded({ extended: true })); // HTML-форми app.post('/user', (req, res) => { console.log(req.body); // { name: 'Alice', age: 30 } res.json(req.body); }); app.listen(3000); // curl -X POST -H "Content-Type: application/json" \ // -d '{"name":"Alice","age":30}' http://localhost:3000/user ``` Middleware спрацьовує до обробника маршруту. Коли код доходить до `req.body`, дані вже розпаршені. ### Як це працює всередині Node.js надає тіло запиту як readable stream через `req.on('data')`. Middleware `express.json()` буферизує вхідні чанки до ліміту розміру (1mb за замовчуванням), потім перевіряє заголовок `Content-Type`. Якщо він збігається з `application/json`, викликає `JSON.parse()` на зібраному UTF-8 рядку і записує результат у `req.body`. Якщо JSON кривий, кидає 400-помилку ще до того як маршрут отримає запит. `express.urlencoded()` робить те саме, але для рядків виду `name=Alice&age=30`. При `extended: true` використовує бібліотеку `qs`, при `extended: false` - вбудований модуль `querystring`. До Express 4.16 для цього потрібен був окремий пакет `body-parser`. Зараз він вбудований, але `body-parser` досі можна підключити для нестандартних налаштувань. ### Коли що використовувати - JSON API ендпоінти (`/users`, `/products`) - `express.json()` - HTML-форми (логін, реєстрація) - `express.urlencoded({ extended: true })` - Завантаження файлів (`<form enctype="multipart/form-data">`) - `multer`, не вбудовані парсери - Stripe-вебхуки і верифікація підпису - `express.raw({ type: '*/*' })` для сирих байтів - GET-маршрути, статичні файли - парсери не потрібні, пропусти для економії пам'яті ### Опція `extended` `extended: false` використовує вбудований `querystring`, який підтримує лише плоскі пари ключ-значення. `extended: true` підключає `qs` з підтримкою вкладених об'єктів: ```js // extended: false // Вхід: user[name]=Alice&user[age]=30 // Вихід: { 'user[name]': 'Alice', 'user[age]': '30' } <- плоскі рядки // extended: true // Вхід: user[name]=Alice&user[age]=30 // Вихід: { user: { name: 'Alice', age: '30' } } <- правильне вкладення ``` Для більшості реальних додатків `extended: true` - правильний вибір. ### Типові помилки **Парсер зареєстровано після маршруту** ```js app.post('/user', handler); // req.body = undefined app.use(express.json()); // запізно - потік вже прочитано ``` Потік читається один раз. Маршрут спрацьовує першим, парсер даних не бачить. Ця помилка з порядком реєстрації зустрічається у кожного Express-розробника хоч раз: логи скрізь, мережа виглядає нормально, а `req.body` все одно `undefined`, і тільки потім помічаєш що `app.use()` стоїть на три рядки нижче маршруту. Перенеси на початок. **Немає заголовка Content-Type на клієнті** ```js // Надсилає дані без вказівки формату: fetch('/user', { method: 'POST', body: JSON.stringify(data) }); // Правильно: fetch('/user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` Парсер перевіряє заголовок перед тим як читати потік. Без збігу `req.body` залишається `{}` або `undefined`. **Вбудований парсер для завантаження файлів** `<form enctype="multipart/form-data">` надсилає `multipart/form-data`. Вбудовані парсери цей формат повністю ігнорують, `req.body` буде `{}`. Для файлів потрібен `multer`. **Немає ліміту розміру для недовірених даних** ```js app.use(express.json()); // ліміт 1mb за замовчуванням // Для публічних auth-ендпоінтів - явний ліміт: app.post('/login', express.json({ limit: '10kb' }), handler); ``` Клієнт, що надсилає 500mb, збуферизує все в пам'яті перед тим як парсер відхилить запит. Це прямий DoS-вектор. Встановлюй явні ліміти для маршрутів що приймають дані від сторонніх. **`extended: false` з вкладеними даними форми** ```js app.use(express.urlencoded({ extended: false })); // Форма: user[profile][name]=Alice // req.body = { 'user[profile][name]': 'Alice' } <- ключі стають рядками ``` Переходь на `extended: true` якщо форми надсилають вкладені структури. ### Де зустрічається - React/Next.js API-маршрути: `express.json()` для REST-ендпоінтів що приймають `fetch` з `JSON.stringify` - HTML-форми логіну та реєстрації: `express.urlencoded({ extended: true })` - Stripe-вебхуки: `express.raw({ type: '*/*' })` для верифікації HMAC-підпису по сирих байтах - Завантаження файлів: `multer` з `diskStorage` або `memoryStorage` залежно від того, пишеш на диск чи стримиш в S3 - Prisma/Drizzle: парсуй тіло, валідуй через Zod або Joi, потім передавай в ORM ### Питання для поглиблення **Q:** Що станеться, якщо зареєструвати `express.json()` двічі? **A:** Обидва парсери запустяться послідовно. Перший буферизує і розпаршує потік. Другий отримає порожній потік і нічого корисного не зробить, але витратить CPU. Реєструй один раз глобально. **Q:** В чому різниця між `express.json()` і `express.raw()`? **A:** `express.json()` повертає готовий JS-об'єкт. `express.raw()` дає сирий `Buffer` без парсингу. Верифікація Stripe-вебхуків вимагає `express.raw()`, бо HMAC-підпис рахується по точних байтах, а не по розпаршеному об'єкту. **Q:** Чому `req.body` залишається `undefined` навіть після `express.json()`? **A:** Три типові причини: парсер зареєстровано після маршруту, клієнт не надсилає `Content-Type: application/json`, або тіло у форматі `multipart/form-data` який парсер не підтримує. **Q:** Як парсинг тіла працював до Express 4.16? **A:** Встановлювали окремий пакет `body-parser` і підключали `app.use(bodyParser.json())`. З 4.16 він вбудований. Сам пакет досі працює і корисний для специфічних налаштувань. **Q:** Навантаження на пам'ять від 1000 одночасних запитів по 1mb на сервер з 4GB RAM? **A:** Кожен запит буферизує приблизно 1mb байтів плюс розпаршений об'єкт - разом близько 2mb. 1000 запитів одночасно дають орієнтовно 2GB піку. Для великих payload-ів використовуй стрімінг через `req.on('data')` і моніторинг через `process.memoryUsage()` або `clinic.js`. ## Приклади ### Базовий: JSON API ендпоінт ```js const express = require('express'); const app = express(); app.use(express.json()); app.post('/register', (req, res) => { const { email, password } = req.body; // { email: 'user@example.com', password: 'secret' } console.log('Реєстрація:', email); res.status(201).json({ message: 'Користувача створено' }); }); app.listen(3000); ``` `express.json()` спрацьовує до обробника маршруту, тому `req.body` вже є звичайним об'єктом коли твій код запускається. ### Середній: Парсинг з різними лімітами для маршрутів ```js const express = require('express'); const app = express(); // Жорсткий ліміт для auth-ендпоінтів app.post('/login', express.json({ limit: '10kb' }), (req, res) => { const { email, password } = req.body; res.json({ token: 'abc123' }); } ); // Stripe-вебхук потребує сирих байтів, не розпаршеного JSON app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; // req.body тут Buffer - потрібен для HMAC-верифікації console.log(Buffer.isBuffer(req.body)); // true res.sendStatus(200); } ); app.listen(3000); ``` Парсери на рівні маршрутів дозволяють застосовувати різні стратегії без єдиного глобального конфігу що впливає на все. ### Розширений: Завантаження файлів через multer ```js const express = require('express'); const multer = require('multer'); const app = express(); app.use(express.json()); const upload = multer({ dest: 'uploads/', limits: { fileSize: 5 * 1024 * 1024 } // 5MB }); app.post('/avatar', upload.single('avatar'), (req, res) => { // req.file -> { fieldname, originalname, mimetype, size, path } // req.body -> інші текстові поля тієї ж форми console.log(req.file.originalname); // 'profile.jpg' res.json({ filename: req.file.filename }); }); app.listen(3000); ``` `multer` обробляє `multipart/form-data` який вбудовані парсери пропускають. Текстові поля з тієї ж форми потрапляють у `req.body`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.