LevelUpWith

Explore / REST APIs: Design, Build, Document and Test / Lesson 1

Lesson 1: Anatomy of an HTTP Request and Response

Learn what goes over the wire when a client talks to an API: the request line, headers and body going out, and the status line, headers and body coming back, as described in RFC 9110 (HTTP Semantics).

Free to read, no account needed. Part of REST APIs: Design, Build, Document and Test.

By the end of this level you'll be able to

  • Send a GET request with curl -v and point out the method, path, request headers, status code, response headers and body in the output
  • Explain that HTTP is stateless and what that means for how an API is designed
  • Tell apart the parts of a URL (scheme, host, path, query string) in a real API call

What actually goes over the wire

Every API call you'll ever make, whether from curl, Python, a browser or a phone app, comes down to the same exchange. The client sends one request message and the server sends back one response message. RFC 9110 (HTTP Semantics, June 2022) defines what these messages mean. RFC 9112 defines how HTTP/1.1 writes them out as text.

Think of it like posting a letter. The envelope has an address and some labels, then there's the letter itself. A request has three parts:

  1. Request line: the method, the target and the HTTP version, e.g. GET /get?lang=en HTTP/1.1
  2. Headers: Name: value lines that carry metadata (who you are, what formats you accept)
  3. Body (optional): the payload. A GET usually has none.

The response has the same layout:

  1. Status line: the version, then a numeric status code, then (in HTTP/1.1) a short reason phrase, e.g. HTTP/1.1 200 OK
  2. Headers: metadata about the response (content type, length, date)
  3. Body: the data, often JSON

An empty line separates the headers from the body. That's how the receiver knows the metadata is finished and the payload is starting. Once you can read these parts, debugging an API stops being guesswork.

Walkthrough: watch a request with curl -v

We'll use httpbin.org, a free service that echoes your request back as JSON.

# -v (verbose) prints the conversation, not just the body
# --http1.1 forces the classic text format so you see the reason phrase
curl -v --http1.1 "https://httpbin.org/get?lang=en&page=2"

Here's the output, trimmed and annotated:

* Connected to httpbin.org port 443          <- '*' = curl telling you what it's doing
> GET /get?lang=en&page=2 HTTP/1.1           <- '>' = sent by you: REQUEST LINE
> Host: httpbin.org                          <- request headers
> User-Agent: curl/8.x
> Accept: */*
>                                            <- empty line: end of request headers (no body)
< HTTP/1.1 200 OK                            <- '<' = received: STATUS LINE
< Content-Type: application/json             <- response headers
< Content-Length: 270
<                                            <- empty line: headers done, body follows
{
  "args": { "lang": "en", "page": "2" },     <- RESPONSE BODY
  "headers": { "Host": "httpbin.org", ... },
  "url": "https://httpbin.org/get?lang=en&page=2"
}

The rule for reading it:

  • > lines are what curl sent
  • < lines are what curl received
  • * lines are curl's own notes (DNS, connection, TLS)
  • Lines with no prefix are the body

Gotcha: HTTP/2 looks different. Leave out --http1.1 and curl will usually use HTTP/2 over HTTPS. Then you'll see < HTTP/2 200 with no reason phrase, and header names in lowercase (content-type:). HTTP/2 dropped the reason phrase and requires lowercase field names on the wire. The meaning is the same either way, and header names are case-insensitive, so your code should never depend on their capitalisation.

The same call from Python

# pip install requests
import requests

r = requests.get("https://httpbin.org/get", params={"lang": "en", "page": 2})

print(r.request.method, r.request.url)    # GET https://httpbin.org/get?lang=en&page=2
print(r.status_code, r.reason)            # 200 OK   (from the status line)
print(r.headers["content-type"])          # application/json  (lookup ignores case)
print(r.json()["args"])                   # {'lang': 'en', 'page': '2'}  (the body, parsed)

A library hides the raw text, but the same parts are still there: method, URL, status, headers and body. Notice that page came back as the string "2". Everything in a query string is text.

Taking a URL apart

Look at this URL:

https://httpbin.org/get?lang=en&page=2#top
└─┬─┘   └────┬────┘└┬─┘└──────┬──────┘└┬─┘
scheme      host   path   query string  fragment
  • Scheme (https): which protocol to use. With https the default port is 443, and with http it's 80. You can write a port explicitly, as in localhost:8000.
  • Host (httpbin.org): which server to contact. curl sends it in the Host header.
  • Path (/get): which thing on that server you want.
  • Query string (lang=en&page=2): everything after ?, written as key=value pairs joined by &.
  • Fragment (#top): only the client uses it. It is never sent to the server. Look at the curl request line again and you'll see no fragment in it.

You can check this with Python's standard library:

from urllib.parse import urlsplit
u = urlsplit("https://httpbin.org/get?lang=en&page=2#top")
print(u.scheme, u.netloc, u.path, u.query, u.fragment)
# https httpbin.org /get lang=en&page=2 top

HTTP is stateless, and why that shapes APIs

RFC 9110 calls HTTP a stateless protocol: each request can be understood on its own. The server isn't required to remember your previous request. Think of a call centre where every call reaches a different agent who has no notes about you. You have to give your reference number every time.

This has practical consequences for API design:

  • Every request carries its own context. Credentials, the page you want and the filters you've chosen all travel in each request, usually as headers or the query string. Asking for page 3 means sending page=3, not "next".
  • Any server can answer. No conversation has to be stored, so a load balancer can send request 1 to server A and request 2 to server B. This is a big reason HTTP APIs scale well.
  • "State" is something the client sends back. Cookies and tokens don't make HTTP stateful. They're the client handing over its reference number again with each request.

Trade-off: requests get a little bigger because they repeat context. In return, servers become interchangeable and failures are much easier to reason about. Production systems take that deal almost every time.

Try it yourself

curl -v --http1.1 "https://httpbin.org/get?colour=blue#ignored" -H "X-Demo: hello"

Check your result: your > lines should contain GET /get?colour=blue HTTP/1.1 with no #ignored, plus X-Demo: hello. The status line should read HTTP/1.1 200 OK. The JSON body should show "colour": "blue" under args and your X-Demo header under headers.

Key takeaways

  • A request is a request line (method, target, version), then headers, then an optional body.
  • A response is a status line (version, status code, and in HTTP/1.1 a reason phrase), then headers, then a body.
  • An empty line separates the headers from the body in both.
  • In curl -v output, > means sent, < means received and * is curl's own commentary.
  • A URL breaks into scheme, host, path, query string and fragment. The fragment never reaches the server.
  • HTTP is stateless, so every request must carry everything the server needs. That's what lets any server handle any request.

A 6-question knowledge check follows this lesson in the app, free and without an account.

More like this

  • GitHub Copilot (GH-300) Exam Prep

    Learn GitHub Copilot hands-on in the editor, in the CLI and on github.com, and work through every skill in the GH-300 study guide (skills measured as of August…

    Exam prep · GH-300 · Software & Engineering · Capable · 18 levels · 4 free · Professionally curated by levelupwith.com

  • Scaling Apps Beyond the Chat Window: Part 3, Mobile Apps, Games and Growth

    Turn your first live app into a real product. Learn to choose between web and app store apps, publish a browser game, take payments safely, run and monitor a l…

    Course · Software · Software & Engineering · Capable · 24 levels · 4 free · Professionally curated by levelupwith.com

  • Scaling Apps Beyond the Chat Window: Part 2, Online, Secure and AI-Powered

    Take one small app from your GitHub repository to a public web address. You will host it on Vercel and give it a Supabase database, sign-in and file storage, w…

    Course · Software · Software & Engineering · Capable · 16 levels · 4 free · Professionally curated by levelupwith.com

  • Scaling Apps Beyond the Chat Window: Part 1, From Prompt to Project

    Move an app you built in an AI chat onto your own computer and work on it step by step with a coding agent. Save your work with Git, keep secrets out of your c…

    Course · Software · Software & Engineering · Capable · 19 levels · 4 free · Professionally curated by levelupwith.com

  • Right Chip, Right Job: CPUs, GPUs, TPUs and Beyond

    Profile a workload, place it on a roofline and pick the processor that actually suits it, whether CPU, GPU, TPU, ASIC, inference chip, NPU, FPGA or DPU. You wi…

    Beyond the Obvious · Software & Engineering · Expert · 61 levels · 4 free · Professionally curated by levelupwith.com

  • Build Your First Website with HTML and CSS

    Start from a blank file and build a real, accessible personal or small-business website that works on phones. Each level adds to the same site, covering semant…

    Course · Software · Software & Engineering · Capable · 18 levels · 4 free · Professionally curated by levelupwith.com