NodeJSParsing Request Bodies

Parsing Request Bodies

HTTP requests carry their payload in the body — but the body arrives as a raw stream of bytes. Parsing turns those bytes into something usable on req.body, and which parser you need depends entirely on the request's Content-Type. JSON, URL-encoded forms, multipart file uploads, and raw binary each need different handling. This page pulls together the built-in parsers and the multipart story into one mental model.

Content-Type decides the parser

Content-Type

Parser

req.body becomes

application/json

express.json()

A parsed object/array

application/x-www-form-urlencoded

express.urlencoded()

A key/value object

multipart/form-data

multer (npm)

Fields + files

text/plain

express.text()

A string

application/octet-stream

express.raw()

A Buffer

No parser, no `req.body` — and the wrong Content-Type silently skips it
Each parser only runs for requests matching its `type`. Register none and `req.body` is `undefined`. Register `express.json()` but have the client POST without `Content-Type: application/json` and the parser **skips** the request, again leaving `req.body` empty. When a body "won't parse", check the request's Content-Type header before anything else.
JSON bodies

JS
app.use(express.json({ limit: '1mb' }))

app.post('/api/users', (req, res) => {
  const { name, email } = req.body       // parsed object
  res.status(201).json({ name, email })
})
The most common API body — but still validate it
JSON is the default for APIs and SPAs. `express.json()` parses and populates `req.body`, but the result is **untrusted input**: a client can send any shape. Validate types and required fields ([request validation](/nodejs/request-validation)) before using them. Parsing ≠ validating.
URL-encoded form bodies

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

// <form method="post" action="/contact">
//   <input name="email"> <textarea name="message">
// </form>
app.post('/contact', (req, res) => {
  const { email, message } = req.body
  res.send('Thanks!')
})
`extended` picks the parsing library
Classic HTML `<form>` submissions (without `enctype`) send `application/x-www-form-urlencoded`. `extended: false` uses Node's `querystring` (flat keys only); `extended: true` uses `qs`, which decodes nested fields like `address[city]=Paris` into nested objects. Choose based on whether your forms use bracketed/nested names — see the [built-in middleware](/nodejs/built-in-middleware) discussion.
Multipart / file uploads — multer

File uploads use multipart/form-data, which the built-in parsers do not handle. The standard tool is multer, which writes uploads to disk (or memory) and fills req.file/req.files plus req.body for text fields:

JS
import multer from 'multer'

const upload = multer({
  dest: 'uploads/',
  limits: { fileSize: 5 * 1024 * 1024 },     // 5 MB cap
  fileFilter: (req, file, cb) => {
    // Accept only images — reject everything else:
    cb(null, file.mimetype.startsWith('image/'))
  },
})

// 'avatar' = the form field name; .single() = one file:
app.post('/profile', upload.single('avatar'), (req, res) => {
  res.json({ file: req.file, fields: req.body })
})
Uploads are a top attack vector — limit size and validate type & destination
Unbounded uploads exhaust disk and memory; always set `limits.fileSize`. Never trust the client-supplied filename or MIME type alone — an attacker can name a script `photo.jpg` or send a fake `Content-Type`. Generate your own safe filenames, store outside the web root, and verify the real file type (magic bytes) for sensitive flows. A naïve upload endpoint is a classic path-traversal / remote-code-execution hole.
Memory vs disk storage
`multer({ dest })` streams files to disk; `multer({ storage: multer.memoryStorage() })` keeps them in `req.file.buffer`. Memory storage is convenient for small files you'll forward to cloud storage immediately, but a large or high-volume upload in memory will OOM the process. Use disk (or stream directly to S3) for anything sizeable.
Raw and text bodies

JS
// Webhook needing exact bytes for signature verification:
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  verify(req.body, req.get('X-Signature'))   // req.body is a Buffer
  res.sendStatus(200)
})

// A plain-text endpoint:
app.post('/notes', express.text(), (req, res) => {
  saveNote(req.body)                          // req.body is a string
  res.sendStatus(201)
})
Webhooks must parse RAW — apply per-route, not globally
Signature verification hashes the *exact* request bytes. If a global `express.json()` parsed and re-stringified the body first, the bytes change and verification fails. Attach `express.raw()` to the webhook route specifically while the rest of the app uses JSON — [per-route parsers](/nodejs/built-in-middleware) take precedence for that route.
Always cap the body size

JS
// A tiny limit on auth endpoints; larger where genuinely needed:
app.use('/auth', express.json({ limit: '10kb' }))
app.use('/import', express.json({ limit: '10mb' }))
The `limit` option is a DoS control, not a formality
Without a `limit`, a malicious client can stream a multi-gigabyte body and exhaust memory before you ever inspect it. The default is **100kb** — fine for typical JSON, but set it explicitly per route: small for login/auth (no legitimate large body), larger only where bulk data truly flows. Over-limit requests are rejected with `413 Payload Too Large` — handle that in your [error middleware](/nodejs/error-middleware).
Decision guide
  • JSON API or SPA → express.json().

  • Classic HTML form → express.urlencoded({ extended: true }).

  • File upload → multer (it handles multipart/form-data).

  • Webhook signature verification → express.raw() on that route.

  • Plain text payload → express.text().

  • Whatever you choose, set a limit and validate the result.

Next
Let browsers on other origins call your API safely: [CORS](/nodejs/cors).