Skip to content
Express.js ex requests 4 min read

Parsing the Request Body

HTTP request bodies arrive as a raw stream of bytes, and Express does not decode them for you by default. Before req.body holds a usable value, you have to mount a body-parsing middleware that reads the stream, matches the request’s Content-Type, and turns the payload into a JavaScript value. Modern Express (4.16+) ships these parsers built in — express.json(), express.urlencoded(), express.raw(), and express.text() — so you no longer need the separate body-parser package for the common cases.

Why the body needs parsing

A route handler that reads req.body without a parser mounted gets undefined. That is by design: parsing every request would waste memory on GET requests that have no body, and different endpoints expect different formats (JSON, HTML form posts, raw binary uploads). You opt in per format, and each parser only activates when the incoming Content-Type matches the type it handles.

const express = require("express");
const app = express();

app.use(express.json());                          // application/json
app.use(express.urlencoded({ extended: true }));  // application/x-www-form-urlencoded

app.post("/users", async (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: "name and email are required" });
  }
  res.status(201).json({ id: 1, name, email });
});

app.listen(3000);

A request of POST /users with header Content-Type: application/json and body {"name":"Ada","email":"[email protected]"} produces:

Output:

{ "id": 1, "name": "Ada", "email": "[email protected]" }

Parsing JSON with express.json()

express.json() reads the stream, buffers it, and runs JSON.parse on the result. The parsed object becomes req.body. If the payload is malformed JSON, the parser calls next(err) with a 400-flavoured error, which your error-handling middleware can catch.

app.use(express.json({ limit: "1mb", strict: true }));

// Catch JSON syntax errors from the parser
app.use((err, req, res, next) => {
  if (err.type === "entity.parse.failed") {
    return res.status(400).json({ error: "Invalid JSON body" });
  }
  next(err);
});

With strict: true (the default), only objects and arrays are accepted at the top level; a bare 42 or "hi" is rejected. Set strict: false to allow any JSON-parseable primitive.

Parsing form posts with express.urlencoded()

HTML forms submitted without enctype send application/x-www-form-urlencoded data — key/value pairs like name=Ada&email=ada%40x.io. express.urlencoded() decodes them into req.body.

app.use(express.urlencoded({ extended: true }));

app.post("/contact", (req, res) => {
  console.log(req.body); // { name: 'Ada', topic: { kind: 'sales' } }
  res.redirect("/thanks");
});

The extended option chooses the decoder. With extended: true (recommended), the qs library parses rich, nested structures like topic[kind]=sales. With extended: false, the built-in querystring module produces only flat string values.

Optionextended: trueextended: false
Libraryqsquerystring
Nested objects (a[b]=c)YesNo
Arrays (a[]=1&a[]=2)YesLimited
Recommended forMost appsFlat, simple forms

Raw and text bodies

Some endpoints need the body untouched — webhook signature verification, file uploads, or plain-text payloads. express.raw() exposes the body as a Node Buffer, while express.text() decodes it to a string.

// Stripe-style webhook: verify the signature over the exact bytes
app.post(
  "/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("Stripe-Signature");
    const rawBuffer = req.body; // Buffer, not parsed JSON
    verify(rawBuffer, signature); // hash must match the untouched bytes
    res.sendStatus(200);
  }
);

// Plain-text ingest
app.post("/notes", express.text({ type: "text/plain" }), (req, res) => {
  res.json({ length: req.body.length });
});

Gotcha: If a global express.json() runs before your webhook route, it consumes and re-encodes the body, breaking signature checks. Mount express.raw() on the webhook route specifically — and place it before any global JSON parser — so the original bytes survive.

Size limits and content-type matching

Every parser accepts a limit to cap the payload size (default 100kb) and a type to control which Content-Type values it handles. Exceeding the limit raises a 413 Payload Too Large error.

app.use(express.json({ limit: "500kb" }));
app.use(express.json({ type: "application/vnd.api+json" })); // JSON:API media type
OptionApplies toDefaultPurpose
limitall parsers"100kb"Maximum body size
typeall parsersper-parserWhich Content-Type to match
inflateall parserstrueDecompress gzip/deflate bodies
strictjsontrueOnly accept objects/arrays
extendedurlencodedvariesRich vs. flat parsing

Tip: The parser is keyed entirely on Content-Type. A JSON payload sent without the application/json header silently leaves req.body as {} — one of the most common “my body is empty” bugs.

Express 5 notes

The built-in parsers behave identically in Express 5.x, and req.body still defaults to undefined until a parser populates it. Express 5 no longer depends on the deprecated standalone body-parser internally, but the express.json / express.urlencoded / express.raw / express.text API is unchanged, so existing parser configuration carries over without edits.

Best Practices

  • Mount express.json() and express.urlencoded() early, before your routes, so req.body is ready everywhere it is needed.
  • Always set an explicit limit to defend against oversized payloads and memory exhaustion.
  • Add an error handler that catches entity.parse.failed and returns a clean 400 instead of leaking parser stack traces.
  • Use route-specific express.raw() for webhooks, and keep it ahead of any global JSON parser so signatures verify against untouched bytes.
  • Prefer extended: true for urlencoded unless you truly only handle flat key/value forms.
  • Validate and coerce req.body fields after parsing — a parsed body is well-formed, not trustworthy.
Last updated June 14, 2026
Was this helpful?