Як працює парсинг тіла в 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, не вбудованих парсерів
Базовий приклад
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/userMiddleware спрацьовує до обробника маршруту. Коли код доходить до 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 з підтримкою вкладених об'єктів:
// 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 - правильний вибір.
Типові помилки
Парсер зареєстровано після маршруту
app.post('/user', handler); // req.body = undefined
app.use(express.json()); // запізно - потік вже прочитаноПотік читається один раз. Маршрут спрацьовує першим, парсер даних не бачить. Ця помилка з порядком реєстрації зустрічається у кожного Express-розробника хоч раз: логи скрізь, мережа виглядає нормально, а req.body все одно undefined, і тільки потім помічаєш що app.use() стоїть на три рядки нижче маршруту. Перенеси на початок.
Немає заголовка Content-Type на клієнті
// Надсилає дані без вказівки формату:
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.
Немає ліміту розміру для недовірених даних
app.use(express.json()); // ліміт 1mb за замовчуванням
// Для публічних auth-ендпоінтів - явний ліміт:
app.post('/login', express.json({ limit: '10kb' }), handler);Клієнт, що надсилає 500mb, збуферизує все в пам'яті перед тим як парсер відхилить запит. Це прямий DoS-вектор. Встановлюй явні ліміти для маршрутів що приймають дані від сторонніх.
extended: false з вкладеними даними форми
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 ендпоінт
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 вже є звичайним об'єктом коли твій код запускається.
Середній: Парсинг з різними лімітами для маршрутів
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
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.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.