hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses HTTP from scratch

What is HTTP

  • The request-response model
  • Anatomy of an HTTP request
  • Anatomy of an HTTP response

Methods

  • GET and HEAD
  • POST
  • PUT, PATCH, and DELETE
  • OPTIONS and CORS preflight

Status codes

  • 2xx success
  • 3xx redirection
  • 4xx client errors
  • 5xx server errors

Headers

  • Request headers
  • Response headers
  • Custom headers

The body

  • JSON
  • Form data and multipart
  • No body

Connections

  • TCP, DNS, and TLS
  • HTTP/1.1 vs HTTP/2
  • Cookies and state

Putting it all together

  • Building a server from scratch
  • From scratch to framework

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 set Content-Type manually. fetch generates 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

FormatContent-TypeUse case
JSONapplication/jsonAPIs, structured data
URL-encodedapplication/x-www-form-urlencodedSimple HTML forms, login forms
Multipartmultipart/form-dataFile 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?

← JSON No body →

© 2026 hectoday. All rights reserved.