Skip to content
Express.js ex libraries 4 min read

express.json & body-parser

When a client sends a POST, PUT, or PATCH request, the payload arrives as a raw stream of bytes — Express does not decode it for you. Body-parsing middleware reads that stream, parses it according to the Content-Type header, and attaches the result to req.body. Modern Express ships these parsers built in as express.json() and express.urlencoded(), so the once-ubiquitous standalone body-parser package is no longer needed for the common cases.

A short history: body-parser is now built in

For years you installed the body-parser package separately and wrote bodyParser.json(). Since Express 4.16, the same parsers are re-exported directly from Express, so you can drop the dependency. The two forms are functionally identical:

// Old style — extra dependency
const bodyParser = require("body-parser");
app.use(bodyParser.json());

// Modern style — built in, no install needed
app.use(express.json());

You only still need the standalone package for exotic parsers it exposes that Express does not re-export. For everyday JSON and form data, prefer the built-ins.

Parsing JSON bodies

express.json() returns middleware that parses requests whose Content-Type is application/json. After it runs, the decoded object lives on req.body. Mount it once, before your routes, and every handler downstream sees the parsed body.

const express = require("express");

const app = express();

app.use(express.json());

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

app.listen(3000, () => console.log("Listening on http://localhost:3000"));

Send a JSON request and the parsed fields are available immediately:

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada","email":"[email protected]"}'

Output:

HTTP/1.1 201 Created
Content-Type: application/json

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

If the Content-Type header is missing or is not application/json, express.json() skips the request and leaves req.body as undefined (or {} in Express 5). Always destructure defensively or validate before use.

Parsing URL-encoded form bodies

HTML forms submitted without JavaScript send application/x-www-form-urlencoded data — key=value pairs joined by &. express.urlencoded() decodes those into req.body. The extended option chooses the parsing library: extended: true uses qs and supports nested objects and arrays; extended: false uses the built-in querystring and only handles flat string values.

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

app.post("/submit", (req, res) => {
  // body: "user[name]=Ada&tags[]=admin&tags[]=staff"
  console.log(req.body);
  res.redirect("/thanks");
});

Output:

{ user: { name: 'Ada' }, tags: [ 'admin', 'staff' ] }

With extended: false, the same payload would yield flat keys like { 'user[name]': 'Ada' }, so reach for extended: true whenever your forms use bracket notation.

Common options

Both parsers accept an options object. The most useful settings control payload size, strictness, and which content types are matched.

OptionApplies toDefaultPurpose
limitboth"100kb"Maximum body size; larger requests get 413 Payload Too Large
strictjsontrueWhen true, only accept arrays and objects (reject bare "text"/42)
typebothapplication/json / application/x-www-form-urlencodedOverride which Content-Type triggers the parser
extendedurlencoded(must set)true for nested objects via qs, false for flat values
inflatebothtrueHandle gzip/deflate-compressed request bodies
verifybothCallback (req, res, buf, encoding) to inspect the raw buffer
app.use(
  express.json({
    limit: "1mb", // accept larger payloads, e.g. rich documents
    strict: true, // reject top-level primitives like 42 or "hi"
    type: "application/*+json", // also parse vendor JSON like application/vnd.api+json
  })
);

The limit guard is a cheap but important defense — without it a client could stream a huge body and exhaust memory. The verify hook is handy for webhook signature checks, since it hands you the exact raw bytes before parsing (for example, verifying a Stripe or GitHub HMAC).

Handling parse errors

If a client sends malformed JSON, express.json() calls next(err) with a SyntaxError carrying status: 400 and a body property. Catch it in an error-handling middleware to return a clean response instead of leaking a stack trace.

app.use((err, req, res, next) => {
  if (err.type === "entity.parse.failed") {
    return res.status(400).json({ error: "Invalid JSON in request body" });
  }
  if (err.type === "entity.too.large") {
    return res.status(413).json({ error: "Request body too large" });
  }
  next(err);
});

Output:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{"error":"Invalid JSON in request body"}

Best Practices

  • Use the built-in express.json() and express.urlencoded() rather than installing body-parser — they are the same code, one less dependency.
  • Always pass extended: true to express.urlencoded() when forms use nested or array fields; otherwise values stay flat.
  • Set an explicit limit sized to your real payloads to cap memory use and reject abusive requests with 413.
  • Mount parsers before your routes but consider scoping them per-router so endpoints that take no body (or raw uploads) are not forced through JSON parsing.
  • Add an error-handling middleware that inspects err.type (entity.parse.failed, entity.too.large) to return clean 400/413 responses.
  • Never trust req.body blindly — validate and sanitize it with a schema or validator before persisting or acting on it.
Last updated June 14, 2026
Was this helpful?