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 |
|---|---|---|---|
| Default form submit |
| Text fields |
|
| MIME parts with boundaries | File uploads + fields |
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:
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)Multipart and file uploads — don't hand-roll it
File upload with busboy (streaming)
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)Validate everything from a form
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).