Fix: Express req.body Is undefined
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix req.body being undefined in Express — missing body-parser middleware, wrong Content-Type header, middleware order issues, and multipart form data handling.
The Error
You access req.body in an Express route handler but get undefined:
app.post('/api/users', (req, res) => {
console.log(req.body); // undefined
const { name, email } = req.body; // TypeError: Cannot destructure property 'name' of undefined
});Or req.body is an empty object {} even though you sent data:
curl -X POST http://localhost:3000/api/users \
-d '{"name":"Alice","email":"[email protected]"}'
# Server logs: {} ← Empty, not undefined, but still missing dataOr you get a parse error:
SyntaxError: Unexpected token o in JSON at position 0Why This Happens
Express does not parse request bodies by default. The framework gives you raw access to the request stream and leaves parsing to middleware. Until something reads the stream and writes the result back to req.body, that property stays undefined. The body-parsing pipeline is opt-in, not opt-out, and it has been that way since the framework’s earliest versions.
Once you add a parser, it only activates when the incoming Content-Type matches what the parser expects. express.json() looks for application/json, express.urlencoded() looks for application/x-www-form-urlencoded, and neither handles multipart/form-data. A request that arrives without a Content-Type header, or with one that no registered parser claims, passes straight through and leaves req.body empty.
The most common root causes break down to a small set of patterns:
- Missing
express.json()orexpress.urlencoded()middleware — the most common cause. Without these, Express doesn’t touch the body. - Wrong
Content-Typeheader —express.json()only parses bodies withContent-Type: application/json. Sending form data withapplication/jsonmiddleware won’t work, and vice versa. - Middleware added after route definition — Express executes middleware in registration order. If you define routes before
app.use(express.json()), the body isn’t parsed when the route runs. - Using an old
body-parserpackage incorrectly —body-parserwas a separate package in older Express versions. Express 4.16+ includes it built-in viaexpress.json()andexpress.urlencoded(). - Router-level middleware not applied — if you use Express Router, body-parsing middleware on
appdoesn’t automatically apply to the router unless it’s registered before the router. - Multipart form data —
express.json()andexpress.urlencoded()don’t handlemultipart/form-data(file uploads). Those requiremulteror similar.
Version History That Changes the Failure Mode
The exact code you need depends on which Express major and minor you are running. Before Express 4.16.0 (released September 2017), there was no express.json() or express.urlencoded() at all. You had to install the standalone body-parser package and call bodyParser.json() and bodyParser.urlencoded(). Tutorials written before late 2017 reflect that pattern, and copying them into a modern project leaves you with an unnecessary dependency. Express 4.16+ exposes the same parsers as express.json() and express.urlencoded() — they are thin wrappers around body-parser internally, but you no longer need the separate install.
Express 5.0 entered alpha in 2014 and finally reached a stable release in 2024 after nearly a decade in pre-release. The body-parsing surface is identical to 4.x, but the router is async-aware: thrown errors and rejected promises inside route handlers now propagate to error middleware automatically, so a parser that throws on malformed JSON no longer crashes the process. If you’re on Express 5, a missing Content-Type no longer needs a defensive try/catch wrapper around JSON.parse-style logic. If you’re still on 4.x, an unhandled rejection during body parsing can still take down the worker, so the middleware order discussion below matters more.
multer has its own history that often catches people. The 1.x line is what most tutorials reference. The 2.x line, released in 2024, fixed several long-standing security advisories and slightly tightened how filename handling works on disk storage. The behavior around multipart/form-data is the same, but the file metadata shape and a few option names changed. If you upgraded multer recently and uploads stopped populating req.file, check the changelog before reaching for older Stack Overflow answers.
Fix 1: Add express.json() Middleware
For JSON request bodies (the most common case with REST APIs):
const express = require('express');
const app = express();
// Add BEFORE your route definitions
app.use(express.json());
// Now req.body is parsed
app.post('/api/users', (req, res) => {
console.log(req.body); // { name: 'Alice', email: '[email protected]' }
const { name, email } = req.body;
res.json({ name, email });
});
app.listen(3000);Test with curl — always set the Content-Type header:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"[email protected]"}'Common Mistake: Sending JSON without the
Content-Type: application/jsonheader. Express’s JSON middleware checks the header first — if it’s missing or wrong, the body is not parsed andreq.bodystaysundefined.
Fix 2: Add express.urlencoded() for Form Data
HTML forms and some API clients send data as application/x-www-form-urlencoded. Use express.urlencoded() for this:
app.use(express.json()); // For JSON bodies
app.use(express.urlencoded({ extended: true })); // For form-encoded bodies
app.post('/api/users', (req, res) => {
console.log(req.body); // { name: 'Alice', email: '[email protected]' }
});Test with curl using form encoding:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=Alice&email=alice%40example.com"extended: true vs extended: false:
extended: true— uses theqslibrary, supports nested objects:user[name]=Aliceextended: false— uses the built-inquerystringmodule, flat key-value pairs only
Use extended: true for modern applications.
Fix 3: Fix Middleware Order
Express applies middleware in the order it’s registered. If routes are defined before express.json(), the body won’t be parsed:
// WRONG — route defined before body-parser middleware
app.post('/api/users', (req, res) => {
console.log(req.body); // undefined — body-parser hasn't run yet
});
app.use(express.json()); // Too late for the route above// CORRECT — middleware registered before routes
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes come after
app.post('/api/users', (req, res) => {
console.log(req.body); // { name: 'Alice' } ✓
});Check that your middleware setup follows this pattern:
const express = require('express');
const app = express();
// 1. Logging middleware
app.use(morgan('dev'));
// 2. Body parsing middleware — before routes
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// 3. Auth middleware
app.use(authMiddleware);
// 4. Routes — after all middleware
app.use('/api/users', userRouter);
app.use('/api/posts', postRouter);Fix 4: Fix Router-Level Middleware
When using express.Router(), body-parsing middleware must be applied before the router is mounted, or added to the router itself:
// app.js
const express = require('express');
const app = express();
const userRouter = require('./routes/users');
// Apply body-parser BEFORE mounting routers
app.use(express.json());
app.use('/api/users', userRouter); // Router will have req.body populated// routes/users.js
const express = require('express');
const router = express.Router();
// req.body is available here because express.json() runs in app.js first
router.post('/', (req, res) => {
const { name, email } = req.body;
res.json({ name, email });
});
module.exports = router;Alternatively, add middleware to the router itself:
// routes/users.js
const express = require('express');
const router = express.Router();
// Apply middleware at router level
router.use(express.json());
router.post('/', (req, res) => {
console.log(req.body); // Parsed ✓
});
module.exports = router;Fix 5: Handle Multipart Form Data (File Uploads)
express.json() and express.urlencoded() don’t parse multipart/form-data. Use multer for file uploads and multipart forms:
npm install multerconst multer = require('multer');
// In-memory storage (for processing the file as a buffer)
const upload = multer({ storage: multer.memoryStorage() });
// Disk storage (save directly to disk)
const diskUpload = multer({ dest: 'uploads/' });
// Single file upload — 'avatar' is the form field name
app.post('/api/avatar', upload.single('avatar'), (req, res) => {
console.log(req.file); // File metadata and buffer
console.log(req.body); // Other form fields
res.json({ filename: req.file.originalname });
});
// Multiple files
app.post('/api/photos', upload.array('photos', 10), (req, res) => {
console.log(req.files); // Array of file objects
console.log(req.body); // Other form fields
});Test with curl:
curl -X POST http://localhost:3000/api/avatar \
-F "avatar=@/path/to/photo.jpg" \
-F "username=alice"Note: When the request uses
multipart/form-data, don’t set theContent-Typeheader manually in curl — curl sets it automatically with the correct boundary. Setting it manually breaks the parsing.
Fix 6: Migrate from Standalone body-parser
Older Express code uses the separate body-parser package. Migrate to the built-in methods:
// Old way (body-parser package, still works but unnecessary in Express 4.16+)
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// New way (built-in Express 4.16+)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));Both are functionally identical. The built-in methods delegate to body-parser internally.
Fix 7: Debug What the Client Is Actually Sending
If you’re unsure whether the client is sending the right Content-Type and body format, log the raw request:
app.use((req, res, next) => {
console.log('Content-Type:', req.headers['content-type']);
console.log('Method:', req.method);
next();
});
app.use(express.json());
app.post('/api/test', (req, res) => {
console.log('Body:', req.body);
res.json({ received: req.body });
});Common Content-Type mismatches:
| Client sends | Middleware needed |
|---|---|
application/json | express.json() |
application/x-www-form-urlencoded | express.urlencoded() |
multipart/form-data | multer |
text/plain | Custom middleware or express.text() |
application/octet-stream | express.raw() |
Use express.text() for plain text bodies:
app.use(express.text()); // Parses text/plain as a string
app.post('/webhook', (req, res) => {
console.log(req.body); // The raw string body
});Use express.raw() for binary data:
app.use(express.raw({ type: 'application/octet-stream' }));
app.post('/upload', (req, res) => {
console.log(req.body); // Buffer
});Still Not Working?
Check if a conflicting middleware is consuming the body stream. Some middleware (like raw body parsing for Stripe webhooks) reads the body stream, leaving nothing for express.json() to parse:
// Stripe webhook needs the raw body — must be before express.json()
app.post(
'/webhook/stripe',
express.raw({ type: 'application/json' }), // Raw body for signature verification
(req, res) => {
const rawBody = req.body; // Buffer
// verify Stripe signature, then process
}
);
// All other routes use parsed JSON
app.use(express.json());
app.post('/api/users', (req, res) => {
console.log(req.body); // Normal parsed JSON ✓
});Check for a proxy stripping headers — if Express sits behind an nginx or load balancer that strips the Content-Type header, the body won’t be parsed. Check req.headers['content-type'] to confirm it’s arriving correctly.
Set the body size limit — if the request body exceeds the default limit (100kb for JSON), Express rejects it with a 413 Payload Too Large error before req.body is populated:
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));Check for compression middleware ordering. If you use compression together with a custom body reader, the compressed stream may need to be decoded before parsing. Place compression after parsers in the request pipeline — it only affects the response stream — but make sure no reverse proxy is double-compressing requests. Inspect the request with req.headers['content-encoding']; if it shows gzip or br, Express’s built-in parsers will not transparently decompress it. You need a separate decompression step or terminate the encoding at the proxy.
Watch out for req.body being overwritten downstream. Validation middleware (Joi, Zod, AJV) sometimes reassigns req.body to a sanitized copy. If a later handler complains that a field is missing, log req.body inside the validator both before and after to see what got stripped. Schema-level unknown: false settings in libraries like Joi remove any property not declared in the schema, even if the client sent it.
Reproduce with curl -v instead of Postman. Postman has historically attached headers automatically and silently re-encoded request bodies depending on the body type selected in the UI. When req.body is mysteriously empty only from your app but works in Postman, switch to curl -v to see the exact bytes on the wire. Mismatches between what you think the client sends and what the server actually receives are the most common reason for “it works locally but not in production” body parsing bugs.
Check the Express version with npm ls express. A monorepo or a transitive dependency can pull in an older Express that does not have the built-in parsers. If npm ls express shows two different majors in the tree, the route handler may be running against the older one. Pin a single version at the workspace root and confirm the route file imports from the expected node_modules path.
Disable any global request-body interceptors during diagnosis. OpenTelemetry, APM agents, and some logger middlewares read the body stream early to capture it for tracing. If they do not put the data back into a re-readable stream, the parsers downstream see an exhausted stream and produce an empty req.body. Temporarily remove the instrumentation, retest, and check whether req.body populates. If yes, configure the interceptor to clone the stream or run after the parsers.
For related Express issues, see Fix: Express Cannot GET Route, Fix: Express CORS Not Working, Fix: Express Middleware Not Working, and Fix: Python requests Timeout.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Bun Not Working — Node.js Module Incompatible, Native Addon Fails, or bun test Errors
How to fix Bun runtime issues — Node.js API compatibility, native addons (node-gyp), Bun.serve vs Node http, bun test differences from Jest, and common package incompatibilities.
Fix: Express Middleware Not Working — Order Wrong, Errors Not Caught, or async Errors Silently Dropped
How to fix Express middleware issues — middleware execution order, error-handling middleware signature, async error propagation with next(err), and common middleware misconfigurations.
Fix: Node.js Stream Error — Pipe Not Working, Backpressure, or Premature Close
How to fix Node.js stream issues — pipe and pipeline errors, backpressure handling, Transform streams, async iteration, error propagation, and common stream anti-patterns.