NodeJSHandling Form Data

Handling Form Data

HTML forms submit data in one of two encodings: application/x-www-form-urlencoded (the default — key=value pairs) and multipart/form-data (required for file uploads). Both arrive in the request body as a stream you must read and parse. This page shows how to handle each on raw http, and why you should almost never parse multipart by hand.

The two form encodings

Encoding

Set by

Body looks like

Use for

application/x-www-form-urlencoded

Default form submit

name=Ada&age=36

Text fields

multipart/form-data

enctype on <form>

MIME parts with boundaries

File uploads + fields

The `Content-Type` header tells you which parser to use
Branch on `req.headers['content-type']`: `application/x-www-form-urlencoded` → parse as a query string; `multipart/form-data` → parse MIME parts (use a library). A form that uploads files **must** declare `enctype="multipart/form-data"`, or the files won't be sent — only their filenames.
Parsing urlencoded forms

A urlencoded body is just a query string in the body. Read the stream, then parse it with URLSearchParams — which handles percent-decoding and +-as-space for you:

JS
import { createServer } from 'node:http'

createServer(async (req, res) => {
  if (req.method === 'POST') {
    let body = ''
    for await (const chunk of req) {
      body += chunk
      if (body.length > 1e6) {           // cap size (DoS guard)
        res.writeHead(413).end('Too large'); req.destroy(); return
      }
    }

    const form = new URLSearchParams(body)
    const name = form.get('name')        // 'Ada Lovelace' (decoded)
    const tags = form.getAll('tags')     // ['a', 'b'] for repeated fields

    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ name, tags }))
  }
}).listen(3000)
`URLSearchParams` handles decoding and repeated keys
Don't split on `&` and `=` by hand — `URLSearchParams` correctly percent-decodes values, converts `+` to spaces, and supports repeated keys via `getAll()` (HTML checkboxes/multi-selects send the same name multiple times). It's the same parser used for [query strings](/nodejs/query-strings), applied to the body.
Multipart and file uploads — don't hand-roll it
Parsing `multipart/form-data` by hand is genuinely hard — use a library
Multipart bodies interleave text fields and binary file contents, separated by a boundary string declared in the `Content-Type`. Correctly parsing them means handling boundary detection across chunk splits, per-part headers, encodings, and streaming large files to disk without buffering them in memory. Edge cases abound. **Use a battle-tested library** — `busboy` (streaming, low-level) or `formidable` (higher-level) — rather than writing your own; a naive parser is both buggy and a security risk.

File upload with busboy (streaming)

JS
import { createServer } from 'node:http'
import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import Busboy from 'busboy'              // npm i busboy

createServer((req, res) => {
  if (req.method === 'POST' && req.headers['content-type']?.startsWith('multipart/')) {
    const bb = Busboy({ headers: req.headers, limits: { fileSize: 5e6 } })

    bb.on('field', (name, value) => console.log('field', name, value))
    bb.on('file', (name, stream, info) => {
      // Stream the upload straight to disk — never buffer it whole:
      pipeline(stream, createWriteStream(`./uploads/${Date.now()}-${info.filename}`))
        .catch((err) => console.error(err))
    })
    bb.on('close', () => { res.writeHead(200).end('uploaded') })

    req.pipe(bb)                          // feed the request into the parser
  }
}).listen(3000)
Stream uploads to disk and enforce size limits — uploads are a top attack vector
Never accumulate an uploaded file in memory — a large or malicious upload exhausts RAM. Stream it to disk (or object storage) as it arrives, and set a `fileSize` limit so the parser aborts oversized uploads. Also validate the file **type and content** (not just the client-supplied filename/MIME, which lie), and store with a server-generated name to prevent path-traversal via crafted filenames.
Validate everything from a form
All form input is untrusted — validate and sanitize
Form data comes straight from the user (or an attacker), so never trust it. Check required fields, types, lengths, and formats; reject invalid input with `422`. Escape or sanitize before rendering it back into HTML (XSS) or putting it in a query (injection). The server must enforce every rule — client-side validation is a UX nicety an attacker simply skips.

JS
function validateSignup(form) {
  const errors = {}
  const email = (form.get('email') || '').trim()
  const password = form.get('password') || ''

  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) errors.email = 'Invalid email'
  if (password.length < 8) errors.password = 'Min 8 characters'

  return { valid: Object.keys(errors).length === 0, errors, data: { email } }
}
What frameworks give you

In Express, body-parsing middleware turns all of this into a property: express.urlencoded() populates req.body for urlencoded forms, and multer (built on busboy) handles file uploads. They apply size limits and parsing for you — but they're doing exactly the work shown above. Knowing the raw mechanics means you'll configure them correctly (and debug them when they misbehave).

Next
Read parameters from the URL itself: [Parsing Query Strings](/nodejs/query-strings).