---
title: "02 — HTTP, the Substrate of Web APIs"
uid: http-the-substrate
tags: ["methods", "roadmap:api-design", "status-codes", "headers", "http", "fundamentals"]
excerpt: "HTTP is a line-by-line, stateless request/response conversation — method, path, headers, body in; status, headers, body back. Almost every web-API convention is stacked on top of that one exchange."
date: 2026-08-13T03:28:34+0000
source: https://www.aveshina.my.id/en/blog/http-the-substrate
---

"The thing fetch() uses" was how I used to file HTTP, until I traced one real request and saw the conversation underneath. The idea the whole web-API world rests on: **HTTP is a line-by-line, stateless request/response conversation — method + path + headers + body in, status + headers + body back — and almost every convention we call "API design" is layered on top of that one exchange.** [1][5]

The reason this matters is that HTTP isn't an implementation detail. It's the *shape* of the contract. Once I could see the request/response pair as a single, self-contained conversation, most of "API design" stopped being a pile of rules and became consequences of that shape — verbs mean what they do because the protocol defines them, status codes work because the response always carries one, headers exist because both sides need a place for metadata that isn't the body.

## The request/response conversation

Every interaction is one request and one response [1]. A request has four parts:

```
POST /orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJ...

{"product": "book", "qty": 2}
```

- A **request line** — method (POST), path (/orders), protocol version.
- **Headers** — key/value metadata: what host, what content type, who's calling.
- A blank line.
- An optional **body** — the payload, here JSON.

The response mirrors it almost exactly, except the first line carries a **status code** instead of a method [1][4]:

```
HTTP/1.1 201 Created
Content-Type: application/json
Location: /orders/417

{"id": 417, "status": "pending"}
```

Two details from this shape are load-bearing for everything else:

- **Stateless.** Each request stands alone; the server keeps no memory between them [1]. Anything that needs to persist (login state, carts) has to be sent again every time — which is exactly why cookies, tokens, and sessions exist as workarounds bolted on top.
- **Typed verbs.** The method isn't decorative — it declares intent. GET reads, POST creates, PUT replaces, PATCH partially updates, DELETE removes [2]. The server is free to ignore that intent, but clients and intermediaries (caches, proxies) rely on it.

## Methods, and what they promise

The roadmap groups methods together because they're the verb in every operation [2]. The part I had to memorize cleanly is the two properties each verb carries, because they decide whether a request is safe to retry:

- **Safe** — calling it doesn't change server state (read-only). GET, HEAD, OPTIONS.
- **Idempotent** — calling it N times has the same effect as calling it once. GET, PUT, DELETE (and HEAD, OPTIONS). POST is *not* idempotent — submitting the same order twice creates two orders.

```figure
<svg viewBox="0 0 620 220" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="A two-by-two grid classifying HTTP methods. Horizontal axis: safe on the left, unsafe on the right. Vertical axis: idempotent on top, not idempotent on bottom. GET sits in safe+idempotent. PUT and DELETE sit in unsafe+idempotent. POST sits in unsafe+not idempotent.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- axes labels -->
    <text x="310" y="22" font-size="12" font-weight="700" fill="#475569" text-anchor="middle">safety vs. idempotency</text>
    <text x="160" y="50" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">SAFE</text>
    <text x="460" y="50" font-size="11" font-weight="700" fill="#7f1d1d" text-anchor="middle">UNSAFE</text>
    <text x="30" y="105" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">IDEM-</text>
    <text x="30" y="120" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">POTENT</text>
    <text x="30" y="170" font-size="11" font-weight="700" fill="#500724" text-anchor="middle">NOT IDEM.</text>

    <!-- grid -->
    <rect x="60" y="60" width="280" height="90" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.2"/>
    <rect x="340" y="60" width="240" height="90" rx="8" fill="#fef3c7" stroke="#ca8a04" stroke-width="1.2"/>
    <rect x="340" y="150" width="240" height="60" rx="8" fill="#fee2e2" stroke="#dc2626" stroke-width="1.2"/>

    <!-- method chips -->
    <rect x="170" y="92" width="60" height="26" rx="13" fill="#16a34a"/>
    <text x="200" y="109" font-size="12" font-weight="700" fill="#fff" text-anchor="middle">GET</text>

    <rect x="380" y="80" width="56" height="26" rx="13" fill="#ca8a04"/>
    <text x="408" y="97" font-size="12" font-weight="700" fill="#fff" text-anchor="middle">PUT</text>
    <rect x="450" y="80" width="80" height="26" rx="13" fill="#ca8a04"/>
    <text x="490" y="97" font-size="12" font-weight="700" fill="#fff" text-anchor="middle">DELETE</text>

    <rect x="410" y="167" width="64" height="26" rx="13" fill="#dc2626"/>
    <text x="442" y="184" font-size="12" font-weight="700" fill="#fff" text-anchor="middle">POST</text>
  </g>
</svg>
```

The practical consequence: a flaky network can safely auto-retry GET, PUT, and DELETE — same result. It must **not** blindly retry POST, because the second attempt might be a duplicate. That single distinction is why payment endpoints demand an idempotency key: it manually gives a non-idempotent verb the idempotency the protocol can't.

## Headers: the metadata channel

The body is the payload; **headers** are everything else both sides need to say about it [3]. They're how the parties negotiate content, carry credentials, hint at caching, and pass tracing IDs. The ones I see in nearly every API:

- Content-Type — what the body is (application/json). Without it, the server guesses.
- Accept — what representation the client wants back. This is the lever for **content negotiation**: the same resource can return JSON, XML, or HTML depending on what the client asks for [3].
- Authorization — who the caller is (Bearer <token>, Basic <base64>).
- Cache-Control — how caches may treat the response.

The mental shift: headers aren't a dumping ground. They're a typed channel, and the protocol defines their meaning. Putting auth in a header instead of the body isn't style — it's because intermediaries (proxies, gateways) know to look there.

## Status codes: the response always carries one

Every response begins with a 3-digit status, and the first digit is the only part I actually needed to internalize [4]:

- 2xx — success (200 OK, 201 Created, 204 No Content).
- 3xx — redirection (301, 304 Not Modified for caches).
- 4xx — the *client* messed up (400 Bad Request, 401 Unauthorized, 404 Not Found, 429 Too Many Requests).
- 5xx — the *server* messed up (500, 502, 503).

The line that matters for debugging: **4xx means the server understood the request and is refusing because of something the caller did or sent; 5xx means the server failed to handle it.** A 404 isn't "the service is down" — it's "you asked for something that doesn't exist, fix your request." That single habit — classify by first digit before reaching for a fix — collapsed a lot of confused incident channels.

## Versions: the substrate keeps moving

HTTP isn't frozen. HTTP/1.1 was the workhorse for two decades; **HTTP/2** introduced multiplexing (many requests over one connection); **HTTP/3** moved the transport onto QUIC for better performance on lossy networks [1][6]. For API design the practical impact is small — the request/response conversation looks the same to the application regardless of version — but two consequences are worth knowing. HTTP/2's multiplexing is what makes one-connection-per-client realistic, which matters for gRPC and for chatty APIs. And HTTP/3's connection migration helps mobile clients switching between cell and Wi-Fi. The protocol evolves underneath; the contract shape stays.

## How I use this

Two habits fall straight out of the conversation model. When debugging a failing call, I read it as a single exchange: I check the *request line and headers* first (did I even call the right thing with the right content type?), then the *status code's first digit* (is this my fault or the server's?), and only then the body. When designing an endpoint, I fill in the four slots deliberately — which verb (so retries and caches behave), which headers (so auth and content type are explicit), which status (so the client can branch correctly), which body shape. The protocol gives me the skeleton; the discipline is just not leaving any of those slots to chance.

## References

[1] Cloudflare, "What is HTTP?," 2024. [Online]. Available: [https://www.cloudflare.com/en-gb/learning/ddos/glossary/hypertext-transfer-protocol-http/](https://www.cloudflare.com/en-gb/learning/ddos/glossary/hypertext-transfer-protocol-http/)

[2] Mozilla, "HTTP request methods," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)

[3] Mozilla, "HTTP headers," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)

[4] Mozilla, "HTTP response status codes," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/HTTP/Status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)

[5] Mozilla, "An overview of HTTP," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview](https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview)

[6] Smashing Magazine, "HTTP/3 From A To Z: Core Concepts," 2021. [Online]. Available: [https://www.smashingmagazine.com/2021/08/http3-core-concepts-part1/](https://www.smashingmagazine.com/2021/08/http3-core-concepts-part1/)

```quiz
Q: Which HTTP method is NOT idempotent?
- GET
- PUT
- DELETE
- POST
correct: 3
explain: POST is not idempotent — repeating it can create the resource again. GET, PUT, and DELETE produce the same end state no matter how many times they repeat.

Q: A response carries `404 Not Found`. The first digit tells you…
- the server crashed
- the client made a bad request (asked for something that doesn't exist)
correct: 1
explain: 4xx is a client error. The server understood the request and is refusing because of what the caller asked for.

Q: Where should an access token conventionally travel?
- in the request body, mixed with the payload
- in the Authorization header, separate from the body
correct: 1
explain: Headers are the typed metadata channel. Auth belongs in Authorization so intermediaries and servers know exactly where to find it.

Q: HTTP is described as "stateless." That means…
- the server remembers the previous request automatically
- each request stands alone and the server keeps no memory between requests
correct: 1
explain: Stateless means no built-in memory between requests. Sessions, cookies, and tokens are workarounds layered on top.

Q: Why does it matter that `GET` is "safe" (read-only)?
- because caches and intermediaries can treat it as non-mutating, and a flaky network can auto-retry it
- because GET requests are always faster
correct: 0
explain: Safe methods don't change server state, so caches will store them and clients can retry them freely without side effects.
```
