How does body parsing work in Express.js?
Body parsing in Express.js reads the raw byte stream from an HTTP request body and converts it into a JavaScript object on req.body, based on the Content-Type header.
Theory
TL;DR
- Without a parser,
req.bodyis alwaysundefined, even if the client sent data express.json()handlesapplication/json;express.urlencoded()handles HTML forms- Register parsers before route definitions, not after - streams are one-pass
- Default size limit is 1mb; adjust with
{ limit: '10kb' }per route multipart/form-data(file uploads) needsmulter, not the built-in parsers
Quick example
const express = require('express');
const app = express();
// Parsers BEFORE routes
app.use(express.json()); // application/json
app.use(express.urlencoded({ extended: true })); // HTML forms
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/userThe middleware runs before your route handler. By the time your code reads req.body, the bytes are already parsed.
How it works internally
Node.js exposes the request body as a readable stream via req.on('data'). The express.json() middleware buffers incoming chunks up to the size limit (1mb by default), then checks the Content-Type header. If it matches application/json, it calls JSON.parse() on the concatenated UTF-8 string and attaches the result to req.body. If the JSON is malformed, it throws a 400 error before your route runs.
express.urlencoded() does the same but uses the qs library (when extended: true) or Node's built-in querystring module (when extended: false) to parse name=Alice&age=30 style strings.
Before Express 4.16, you needed the separate body-parser npm package. It ships built-in now, though body-parser is still importable for custom configurations.
When to use each parser
- JSON API endpoints (
/users,/products) -express.json() - HTML form submissions (login, register pages) -
express.urlencoded({ extended: true }) - File uploads (
<form enctype="multipart/form-data">) -multer, not the built-ins - Stripe webhooks needing raw bytes for signature verification -
express.raw({ type: '*/*' }) - GET routes, static files - skip parsers entirely to save memory
The extended option
extended: false uses Node's querystring module, which only handles flat key-value pairs. extended: true uses the qs library, which parses nested objects and arrays:
// extended: false
// Input: user[name]=Alice&user[age]=30
// Output: { 'user[name]': 'Alice', 'user[age]': '30' } <- flat strings
// extended: true
// Input: user[name]=Alice&user[age]=30
// Output: { user: { name: 'Alice', age: '30' } } <- proper nestingFor most real apps, extended: true is what you want.
Common mistakes
Mistake: parser registered after the route
app.post('/user', handler); // req.body = undefined here
app.use(express.json()); // too late - stream already consumedStreams are read once. The route runs first and the parser never sees the data. The parser order bug catches almost every Express developer at least once - you add logging everywhere, the network tab looks fine, but req.body stays undefined, and then you spot the app.use() call sitting three lines below the route. Move it to the top.
Mistake: missing Content-Type header on the client
// This sends data without telling the parser what format it is:
fetch('/user', { method: 'POST', body: JSON.stringify(data) });
// Fix:
fetch('/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});The parser checks the header before touching the stream. No header match means req.body stays {} or undefined.
Mistake: using built-in parsers for file uploads
<form enctype="multipart/form-data"> sends multipart/form-data. The built-in parsers skip this format entirely. req.body will be {}. You need multer for file uploads.
Mistake: no size limit on untrusted input
app.use(express.json()); // 1mb default - fine for most APIs
// For public-facing auth endpoints, be explicit:
app.post('/login', express.json({ limit: '10kb' }), handler);A client sending a 500mb payload buffers everything in memory before the parser rejects it. That is a straightforward denial-of-service vector. Set explicit limits for routes that accept untrusted data.
Mistake: extended: false with nested form data
app.use(express.urlencoded({ extended: false }));
// Form: user[profile][name]=Alice
// req.body = { 'user[profile][name]': 'Alice' } <- keys become strings, no nestingSwitch to extended: true if your forms send nested structures.
Real-world usage
- React/Next.js API routes:
express.json()for REST endpoints receivingfetchwithJSON.stringify - Login and register pages:
express.urlencoded({ extended: true })for browser form submits - Stripe webhooks:
express.raw({ type: '*/*' })so you can verify the HMAC signature against the exact bytes before any parsing - File upload endpoints:
multerwithdiskStorageormemoryStoragedepending on whether you write to disk or stream to S3 - Prisma/Drizzle mutations: parse the body, validate with Zod or Joi, then pass to the ORM
Follow-up questions
Q: What happens if you register express.json() twice?
A: Both parsers run in sequence. The first one buffers and parses the stream. The second one gets an empty stream, does nothing useful, but still spends CPU on the attempt. Register it once globally.
Q: What is the difference between express.json() and express.raw()?
A: express.json() parses the body and gives you a JS object. express.raw() gives you the raw Buffer with no parsing. Stripe webhook verification requires express.raw() because the HMAC signature is computed over the exact bytes, not the parsed object.
Q: Why does req.body stay undefined even after adding express.json()?
A: Three common causes: parser registered after the route, client not sending Content-Type: application/json, or the body is multipart/form-data which the parser skips.
Q: How did body parsing work before Express 4.16?
A: You installed body-parser as a separate package and called app.use(bodyParser.json()). Express 4.16 bundled it. The body-parser package still works and is still useful for per-route options or older setups.
Q: What are the memory implications of 1000 concurrent 1mb JSON requests on a 4GB server?
A: Each request buffers roughly 1mb of raw bytes plus the parsed object overhead - about 2mb total. 1000 concurrent requests hit roughly 2GB peak. For large payloads, process with streaming (req.on('data')) or use worker clusters. Monitor with process.memoryUsage() or clinic.js.
Examples
Basic: JSON API endpoint
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('Registering:', email);
res.status(201).json({ message: 'User created' });
});
app.listen(3000);express.json() runs before the route handler. By the time your code reads req.body, it is already a plain JS object.
Intermediate: Route-specific parsing with different limits
const express = require('express');
const app = express();
// Tight limit for auth endpoints
app.post('/login',
express.json({ limit: '10kb' }),
(req, res) => {
const { email, password } = req.body;
res.json({ token: 'abc123' });
}
);
// Stripe webhook needs raw bytes, not a parsed object
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['stripe-signature'];
// req.body is a Buffer here - needed for HMAC verification
console.log(Buffer.isBuffer(req.body)); // true
res.sendStatus(200);
}
);
app.listen(3000);Route-specific parsers let you apply different strategies per endpoint without one global config affecting everything.
Advanced: File upload with 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 -> other text fields from the same form
console.log(req.file.originalname); // 'profile.jpg'
res.json({ filename: req.file.filename });
});
app.listen(3000);multer handles multipart/form-data that the built-in parsers skip. Text fields from the same form still land on req.body.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.