Skip to main content

Як працює парсинг тіла в Express.js?

Парсинг тіла (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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?