A body arrives as a stream of bytes, so req.body is undefined until something reads that stream and parses it. In Express 4 24,430 that was the separate body-parser package; Express 5 ships the same parsers built in, with an error convention that lets one handler report every way a body can be rejected.
app.use(express.json({ limit: '100kb' })); // application/json
app.use(express.urlencoded({ extended: false })); // HTML form posts
app.post('/books', (req, res) => res.status(201).json(req.body));
app.use((err, req, res, next) => {
if (err.type && err.type.startsWith('entity.')) {
return res.status(err.status).json({ error: err.message, limit: err.limit });
}
next(err);
});$ curl -sX POST .../books -H "Content-Type: application/json" -d '{"year":1965}'
{"year":1965}
$ curl -sX POST .../books -d "year=1965&tags=scifi&tags=classic" # form-encoded
{"year":"1965","tags":["scifi","classic"]}
$ curl -sX POST .../books -H "Content-Type: application/json" -d '{"title":'
{"error":"Unexpected end of JSON input"}The first two responses are the argument for JSON APIs: JSON preserves types, so year returns as the number 1965, while a form body is text and yields "1965". A field sent twice becomes an array either way.
Each parser only touches requests whose Content-Type matches, so mounting both is safe. extended: false uses Node's querystring and is the Express 5 default; extended: true switches to qs and nests address[city]=Ipoh — avoid it unless a form needs nesting, a denial-of-service surface. The default limit of 100 kB is generous for an API and far too small for an upload, which belongs to multer 12,087 (Handling Uploads with Multer).
A body that breaks a rule reaches the error middleware (Error Middleware) carrying status, type and expose: entity.parse.failed with 400, entity.too.large with 413, entity.verify.failed with 403. Without that handler a JSON client gets Express's HTML error page, stack trace included. One last option to know: verify(req, res, buf, encoding) runs before parsing and receives the raw Buffer — how a webhook check keeps the exact bytes Stripe 238 or GitHub 29 signed, which re-serializing cannot reproduce.