Form data and multipart
Before JSON, there were forms
JSON is the default for APIs today, but it was not always that way. Before single-page applications and JavaScript-heavy frontends, HTML forms were the primary way users sent data to servers. And HTML forms do not send JSON. They use a format called URL-encoded data. It is still used today, and you will encounter it, so it is worth understanding.
URL-encoded form data
When you submit a plain HTML <form>, the browser encodes the data like this:
Content-Type: application/x-www-form-urlencoded
title=Kindred&genre=science-fiction&rating=5 Key-value pairs separated by &. Simple enough. Special characters get percent-encoded (spaces become + or %20, & becomes %26, and so on). The Content-Type header tells the server: “This body is URL-encoded form data.”
Parsing form data
On the client side, sending form data looks like this:
const response = await fetch("https://api.example.com/books", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "title=Kindred&genre=science-fiction",
}); On the server side, JavaScript has a built-in class for parsing it:
const text = await request.text();
const params = new URLSearchParams(text);
console.log(params.get("title")); // "Kindred"
console.log(params.get("genre")); // "science-fiction" URLSearchParams takes the raw URL-encoded string and gives you a nice API to read individual values. It handles percent-decoding automatically. You do not have to split on & and decode things yourself.
The problem with files
URL-encoded data works great for text fields. But what about file uploads? A JPEG image is binary data, and trying to percent-encode an entire image would be huge and inefficient. That is why multipart form data exists.
Multipart: sending files and text together
Multipart form data sends each field as a separate “part,” and each part has its own headers and body:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="title"
Kindred
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="cover"
Content-Type: image/jpeg
(binary JPEG data here)
------WebKitFormBoundary7MA4YWxk-- This looks more complicated, so let’s break it down. The Content-Type header includes a boundary string. This boundary is a unique marker that separates each field. The server reads until it finds the boundary, processes that part, then reads until the next boundary.
Each part has a Content-Disposition header that gives the field name. File parts also get their own Content-Type (like image/jpeg). The -- at the very end marks the end of the message.
Why use a boundary instead of something simpler like &? Because binary file data can contain any byte pattern, including &. A boundary is a long, unique string that is guaranteed not to appear in the file data.
Sending multipart from JavaScript
const form = new FormData();
form.append("title", "Kindred");
form.append("cover", fileInput.files[0]); // File from an <input type="file">
const response = await fetch("https://api.example.com/books", {
method: "POST",
body: form,
// Do NOT set Content-Type — fetch sets it automatically with the boundary
}); There is an important catch here: do not set the Content-Type header yourself. fetch sets it automatically when you pass a FormData object, and it includes the correct boundary string. If you set Content-Type manually, the boundary will not match, and the server will fail to parse the parts.
[!WARNING] When using
FormData, do not setContent-Typemanually.fetchgenerates the boundary and sets the header for you. If you override it, the boundary in the header will not match the boundary in the body, and parsing will break.
[!NOTE] The File Uploads course builds a complete upload system using multipart form data. This lesson explains the HTTP format behind it.
When to use which
| Format | Content-Type | Use case |
|---|---|---|
| JSON | application/json | APIs, structured data |
| URL-encoded | application/x-www-form-urlencoded | Simple HTML forms, login forms |
| Multipart | multipart/form-data | File uploads, forms with files |
For APIs, JSON is the standard (and what we use throughout this course series). URL-encoded is the default for HTML forms that do not involve files. Multipart is required when you need to upload files.
The next lesson covers a scenario we keep running into: sometimes there is no body at all.
Exercises
Exercise 1: Submit a URL-encoded form body from a client. Parse it on the server with URLSearchParams.
Exercise 2: Submit a multipart form with a text field and a file. Log the boundary string and each part’s headers on the server.
Exercise 3: Try setting Content-Type manually when sending a FormData body with fetch. Observe the parsing error on the server.
Why does multipart form data use a boundary string?