Skip to content

Fix: Express req.body Is undefined

FixDevs · (Updated: )

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 data

Or you get a parse error:

SyntaxError: Unexpected token o in JSON at position 0

Why 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() or express.urlencoded() middleware — the most common cause. Without these, Express doesn’t touch the body.
  • Wrong Content-Type headerexpress.json() only parses bodies with Content-Type: application/json. Sending form data with application/json middleware 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-parser package incorrectlybody-parser was a separate package in older Express versions. Express 4.16+ includes it built-in via express.json() and express.urlencoded().
  • Router-level middleware not applied — if you use Express Router, body-parsing middleware on app doesn’t automatically apply to the router unless it’s registered before the router.
  • Multipart form dataexpress.json() and express.urlencoded() don’t handle multipart/form-data (file uploads). Those require multer or 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/json header. Express’s JSON middleware checks the header first — if it’s missing or wrong, the body is not parsed and req.body stays undefined.

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 the qs library, supports nested objects: user[name]=Alice
  • extended: false — uses the built-in querystring module, 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 multer
const 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 the Content-Type header 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 sendsMiddleware needed
application/jsonexpress.json()
application/x-www-form-urlencodedexpress.urlencoded()
multipart/form-datamulter
text/plainCustom middleware or express.text()
application/octet-streamexpress.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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles