REST is not HTTP, but practically every REST API in the world travels over HTTP. If you do not understand the protocol, you will end up copying examples without knowing why they work and debugging blind when they stop working. This lesson opens HTTP right up: we will see a raw request and response, what it implies for the protocol to be stateless, how a URL breaks down, which headers turn up in almost every API, why everything must be encrypted and what changes between HTTP/1.1, HTTP/2 and HTTP/3. We will finish by observing real traffic against the Aroma Store API with curl -v and with the browser's tools.

This is a map and fundamentals lesson: we will present methods and status codes as an overview, but their detailed study belongs to lessons 02-03 and 02-04.

Contents

  1. What HTTP is and what role it plays in an API
  2. Anatomy of an HTTP request
  3. Anatomy of an HTTP response
  4. The request-response model and the absence of state
  5. The URL and its parts
  6. Common headers in APIs
  7. Overview of methods and status codes
  8. HTTPS and TLS: why every API is encrypted
  9. HTTP/1.1, HTTP/2 and HTTP/3
  10. Observing HTTP: curl -v and the DevTools

  1. What HTTP is and what role it plays in an API

HTTP (HyperText Transfer Protocol) is an application-layer protocol that defines how a client asks a server for something and how the server responds. Three characteristics define it:

  • It is textual in its classic versions: you can read it with your own eyes, which makes debugging enormously easier.
  • It follows a request-response model: for each request there is exactly one response.
  • It is stateless: the server remembers nothing about previous requests.

When in lesson 01-01 we wrote curl https://api.aromastore.example/v1/coffees, curl built an HTTP message, sent it over a network connection and showed us only the body of the response. Now we are going to see that message in full.

sequenceDiagram
    participant C as Client
    participant S as Aroma Store server
    Note over C,S: 1. The connection is established (TCP + TLS)
    C->>S: Request: request line + headers + body
    Note over S: 2. The server processes it
    S-->>C: Response: status line + headers + body
    Note over C,S: 3. The connection is reused or closed

  1. Anatomy of an HTTP request

Every HTTP request has three parts: request line, headers and, optionally, a body. Here is a complete request to create an order in Aroma Store:

POST /v1/orders HTTP/1.1
Host: api.aromastore.example
Content-Type: application/json
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
User-Agent: AromaMobile/2.4 (Android 14)
Content-Length: 118

{
  "customerId": "cus_842",
  "items": [
    { "coffeeId": "cof_001", "quantity": 2 }
  ]
}

Let's analyse each element:

Request line (POST /v1/orders HTTP/1.1). It has exactly three pieces separated by spaces:

Piece Value Meaning
Method POST The action to be performed (here, creating something)
Path /v1/orders Which resource is the target, without the domain
Version HTTP/1.1 Which version of the protocol the client speaks

Headers: Name: value pairs, one per line. They are metadata about the request: they describe the message, they are not part of the data being sent. Host is mandatory in HTTP/1.1 because a single server can host many domains and needs to know which one the request is aimed at.

Blank line: separates the headers from the body. It is mandatory, and it is the marker that tells the server "the headers have finished".

Body: the data being sent. Here, the order in JSON. GET requests normally carry no body; creation or modification requests do.

Key point: the path goes without the domain in the request line. The domain travels in the Host header. When you type https://api.aromastore.example/v1/orders, the client splits that address into the two parts automatically.

  1. Anatomy of an HTTP response

The response has the same structure, with a different first line:

HTTP/1.1 201 Created
Date: Fri, 14 Aug 2026 09:12:44 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 226
Location: /v1/orders/ord_5001
Cache-Control: no-store

{
  "id": "ord_5001",
  "customerId": "cus_842",
  "status": "pending_payment",
  "totalEuros": 29.00,
  "items": [
    { "coffeeId": "cof_001", "name": "Ethiopia Yirgacheffe", "quantity": 2, "priceEuros": 14.50 }
  ]
}

Status line (HTTP/1.1 201 Created):

Piece Value Meaning
Version HTTP/1.1 The version the server speaks
Code 201 The result as a number, interpretable by machines
Phrase Created A readable description; purely informative

Response headers: here Content-Type indicates that the body is JSON encoded in UTF-8, Content-Length says how many bytes it takes up, Location points to where the newly created resource ended up (a very useful convention we will return to in 02-04) and Cache-Control: no-store forbids storing this response in a cache, which is reasonable for an order.

Body: the representation of the created resource. Note that the server has added information the client did not send: the id, the initial status and the calculated totalEuros. The client does not calculate prices; that is server-side business logic.

  1. The request-response model and the absence of state

HTTP works in strict turns: the client asks, the server answers. The server never starts the conversation (that is what webhooks, Server-Sent Events and WebSockets are for, which we will see in 01-07).

The most important property for us is that HTTP is stateless: each request is independent and the server remembers nothing about the previous ones. If you send these two requests one after the other:

curl https://api.aromastore.example/v1/coffees/cof_001
curl https://api.aromastore.example/v1/coffees/cof_002

the server does not know that the second comes from the same client as the first, unless you tell it explicitly in the request itself.

Practical consequence number one: every request must carry all the information needed to be served, including the identity of whoever makes it. That is why the Authorization header is repeated in each and every authenticated request, rather than "logging in once".

Practical consequence number two: since no request depends on a previous one, any server in a group can serve any request. This is what lets you put ten servers behind a load balancer and scale horizontally.

Beware of a frequent confusion: the protocol having no state does not mean there is no persistent data. The Aroma Store cart is stored in the database and is just another resource (/v1/carts/crt_77); what does not exist is a live "session" in the memory of one particular server. The difference between application state and session state is central to REST and we will return to it in 01-04.

  1. The URL and its parts

A URL uniquely identifies a resource on the network. Let's analyse a complete one from Aroma Store:

https://api.aromastore.example:443/v1/coffees?origin=Ethiopia&roast=light#notes
\___/   \______________________/\_/\_________/\_________________________/ \___/
  1                2             3      4                 5                 6
# Part Example What it is for
1 Scheme https The protocol to use. In APIs, always https
2 Host api.aromastore.example The server to connect to
3 Port 443 TCP port. Omitted if it is the standard one (80 for http, 443 for https)
4 Path /v1/coffees Which resource is requested within that server
5 Query string ?origin=Ethiopia&roast=light Parameters: filters, ordering, pagination
6 Fragment #notes Not sent to the server; only the browser uses it

Details worth internalising:

  • The query string starts with ? and chains key=value pairs separated by &. It is the natural place for whatever modulates the query (filtering, sorting, paginating), not for identifying the resource. We will develop this in 02-06.
  • The fragment never reaches the server. Never use it to carry API data.
  • Values must be encoded (percent-encoding) if they contain special characters: a space is %20, an accented character is encoded as several bytes. That is why in the example we write origin=Ethiopia, and a value such as Perú would travel as origin=Per%C3%BA.

With curl, it is a good idea to quote the URL so that the shell does not interpret the & as "run in the background":

# Correct: the full URL in quotes
curl "https://api.aromastore.example/v1/coffees?origin=Ethiopia&roast=light"

# Incorrect: the shell splits the command at the & and curl only receives up to "Ethiopia"
curl https://api.aromastore.example/v1/coffees?origin=Ethiopia&roast=light

You will also come across the term URI. In web API practice, URI and URL are used as synonyms; formally, a URI is the general concept of an identifier and a URL is an identifier that also says how to locate the resource.

  1. Common headers in APIs

There are dozens of standard headers. These are the ones that will come up again and again in the course:

Header Direction What it is for Example
Content-Type Both Format of this message's body application/json; charset=utf-8
Accept Request Formats the client can receive application/json
Authorization Request Credentials of the caller Bearer eyJhbGci...
User-Agent Request Who the client is (name and version) AromaMobile/2.4 (Android 14)
Content-Length Both Body size in bytes 226
Cache-Control Both Caching policy max-age=300, public
Location Response Address of a created resource or of a redirection /v1/orders/ord_5001
Date Response The moment the response was generated Fri, 14 Aug 2026 09:12:44 GMT

Three important clarifications:

  • Content-Type and Accept are not the same thing. Content-Type describes what I am sending; Accept describes what I want to receive. A POST request usually carries both: "I am sending you JSON and I want JSON back". A GET carries only Accept, because it sends no body. This mechanism is called content negotiation and is the subject of 02-05.
  • Authorization with the Bearer scheme is the dominant pattern today: a token is sent that the server validates. The whole mechanism (JWT, OAuth 2.0) is covered in 03-06 and 04-03.
  • Header names are case-insensitive (content-type is the same as Content-Type), although by convention they are written capitalised. In HTTP/2 and HTTP/3 they always travel in lower case.

There are also custom headers. The modern convention is not to use the X- prefix (discouraged since RFC 6648) and to choose specific names such as Aroma-Request-Id.

  1. Overview of methods and status codes

Here we only sketch the map. The detailed study of each method is in lesson 02-03 and that of each status code in 02-04.

Methods: the verb of the request

The method indicates the intent of the request regarding the resource:

Method Intent Example in Aroma Store
GET Obtain a representation GET /v1/coffees/cof_001
POST Create or process something new POST /v1/orders
PUT Replace entirely PUT /v1/coffees/cof_001
PATCH Modify partially PATCH /v1/coffees/cof_001
DELETE Delete DELETE /v1/carts/crt_77
HEAD Like GET but headers only Check whether something exists or has changed
OPTIONS Ask what is allowed Used by the browser in CORS (04-05)

Two properties worth having on your radar already, because they explain why the choice of method matters:

  • Safe: it modifies nothing on the server. GET and HEAD are. That is why a search engine can crawl links without fear.
  • Idempotent: repeating the same request several times leaves the system in the same state as doing it once. GET, PUT and DELETE are; POST is not. Hence retrying a POST /v1/orders after a network failure can create two orders, a real problem we will tackle.

Status codes: the result in three digits

The first digit determines the family, and with that you already know the essentials:

Family Meaning Frequent examples
1xx Informational (rare in APIs) 100 Continue
2xx Success 200 OK, 201 Created, 204 No Content
3xx Redirection or "use your cache" 301 Moved Permanently, 304 Not Modified
4xx Client error: the request is wrong 400 Bad Request, 401 Unauthorized, 404 Not Found, 422 Unprocessable Content
5xx Server error: the request was valid 500 Internal Server Error, 503 Service Unavailable

The distinction between 4xx and 5xx is one of the most useful there is when debugging: 4xx means "fix the request", 5xx means "the problem is mine". A server that returns 200 OK with a body of {"error": "not found"} is lying to the protocol and breaking all the automated machinery that relies on the code: caches, retries, monitoring and alerting.

  1. HTTPS and TLS: why every API is encrypted

HTTPS is HTTP carried inside a connection encrypted with TLS (Transport Layer Security, the successor to SSL). It provides three guarantees:

  1. Confidentiality: nobody along the way can read the content. Without TLS, the Authorization header with the token travels in plain text and anyone on the same wi-fi network can copy it.
  2. Integrity: nobody can alter the messages without it being detected.
  3. Authenticity: the server's certificate proves you are talking to api.aromastore.example and not to an impostor.

In practice this means:

  • Never publish an API over http://. Not even "just for testing": test URLs end up in production.
  • Redirect http traffic to https with a 301, but do not rely on that as security: the first request already travelled in the clear.
  • Tokens and API keys are only secure if the channel is.
  • The internal API is encrypted too. The internal network is not a trusted place; the zero trust model assumes an attacker is already inside.

Checking an API's certificate with curl:

curl -vI https://api.aromastore.example/v1/coffees 2>&1 | grep -E "SSL|subject|issuer|expire"

We will expand on all this in lesson 04-02, devoted to security.

  1. HTTP/1.1, HTTP/2 and HTTP/3

The conceptual model (request, response, methods, headers, codes) is identical in all three versions. What changes is how the messages are transported:

Aspect HTTP/1.1 (1997) HTTP/2 (2015) HTTP/3 (2022)
Format Text Binary Binary
Transport TCP TCP QUIC over UDP
Simultaneous requests One per connection (in practice, several connections) Multiplexed over one connection Multiplexed, with no blocking on packet loss
Headers Text repeated in every request Compressed (HPACK) Compressed (QPACK)
Main problem it solves Head-of-line blocking in HTTP Head-of-line blocking in TCP

What changes in practice for someone designing an API:

  • Your code does not change. A REST API works the same in all three versions; normally it is the web server or the gateway that decides the version, and the client negotiates the best one available.
  • The cost of making many requests does change. With HTTP/1.1, making 30 calls to render one screen was hugely expensive, and that pushed people towards designing large responses that return everything. With HTTP/2, several small requests are far more affordable. This is an argument that will come up when we discuss GraphQL in 01-07.
  • Cheap headers: with compression, repeating Authorization in every request costs little.
  • HTTP/2 and HTTP/3 require TLS in practice (all browsers demand it), which reinforces the previous point.
  • gRPC builds on HTTP/2 precisely because of multiplexing and bidirectional streaming.

  1. Observing HTTP: curl -v and the DevTools

You cannot learn HTTP without seeing it. Two tools are enough for 95% of cases.

curl -v

The -v (verbose) option shows the whole conversation. Lines starting with > are what the client sends; those starting with < are what the server returns; those starting with * are connection information.

curl -v https://api.aromastore.example/v1/coffees/cof_001

Output (abbreviated and annotated):

* Connected to api.aromastore.example (203.0.113.42) port 443
* SSL connection using TLSv1.3 / AEAD-AES128-GCM-SHA256
> GET /v1/coffees/cof_001 HTTP/2
> Host: api.aromastore.example
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/2 200
< content-type: application/json; charset=utf-8
< cache-control: public, max-age=300
< etag: "a7f3c9"
<
{"id":"cof_001","name":"Ethiopia Yirgacheffe","priceEuros":14.50,"stock":120}

What this output teaches us:

  • curl automatically added Host, User-Agent and Accept: */* ("I accept any format").
  • The connection negotiated HTTP/2 and TLS 1.3, and the response headers arrive in lower case, as befits HTTP/2.
  • The server allows caching for 5 minutes (max-age=300) and sends an ETag, a fingerprint of the content that allows revalidation without downloading it again (lesson 04-06).

curl options we will use throughout the course:

# -i shows the response headers alongside the body (cleaner than -v)
curl -i https://api.aromastore.example/v1/coffees/cof_001

# -X forces the method, -H adds headers, -d sends a body
curl -X POST https://api.aromastore.example/v1/orders \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"customerId":"cus_842","items":[{"coffeeId":"cof_001","quantity":2}]}'

# -I makes a HEAD request: headers only, without downloading the body
curl -I https://api.aromastore.example/v1/coffees

# -o saves to a file and -s silences the progress bar
curl -s https://api.aromastore.example/v1/coffees -o coffees.json

A useful detail: when you use -d, curl already assumes POST and Content-Type: application/x-www-form-urlencoded, so the Content-Type: application/json header is mandatory if you are sending JSON. Forgetting it is one of the most frequent causes of receiving a baffling 400.

Browser DevTools

Press F12 in your browser and open the Network tab. With it you can:

  1. Filter by Fetch/XHR to see only the API calls the page makes, ignoring images and stylesheets.
  2. Click a request and review its tabs: Headers (request line, status code and all the headers), Payload (the body sent), Response (the body received) and Timing (where the time went).
  3. Use Copy as cURL in the context menu: it turns any browser request into a curl command you can reproduce in the terminal. It is the fastest technique for debugging a call that fails on the Aroma Store website.

A very instructive exercise: open any real online shop, filter by Fetch/XHR and watch the calls it makes when you add something to the basket. You will see methods, paths, status codes and JSON bodies live.

Common Mistakes and Tips

  • Sending JSON without Content-Type: application/json. The server does not guess the format; it will treat it as text or as a form and fail to interpret it.
  • Confusing Accept with Content-Type. Mnemonic: Content-Type describes what is inside this envelope; Accept describes what I want back.
  • Putting sensitive data in the query string. URLs end up recorded in server logs, in proxies and in browsing history. Tokens and passwords go in headers or in the body, never in the URL.
  • Forgetting to quote the URL in curl when it contains &. The shell splits it and you get incoherent results.
  • Assuming the server "remembers" the previous request. Stateless means exactly that: every request must be self-sufficient.
  • Returning 200 OK for errors. It breaks caches, automatic retries and monitoring.
  • Worrying about HTTP/2 or HTTP/3 in your API code. That is an infrastructure decision; your job is to design the contract well.
  • Tip: spend fifteen minutes firing curl -v at three or four public APIs you use. Seeing real headers from real services teaches more than any table.

Exercises

Exercise 1: read a raw request

Given this request, answer: (a) what method and path does it use?; (b) which domain is it aimed at?; (c) what format does it send and which does it expect to receive?; (d) does it carry credentials?; (e) what exactly is it doing?

PATCH /v1/coffees/cof_002 HTTP/1.1
Host: api.aromastore.example
Content-Type: application/json
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

{ "priceEuros": 13.50 }

Exercise 2: break down a URL

Break this URL down into its six parts and state which of them does not reach the server. Also explain whether ?roast=light&limit=10 identifies a different resource or modulates a query.

https://api.aromastore.example/v1/coffees?roast=light&limit=10#results

Exercise 3: build requests with curl

Write the curl commands to:

  1. Get the coffee cof_001, showing the response headers.
  2. Check whether the resource /v1/coffees/cof_999 exists without downloading the body.
  3. Create a review with POST /v1/reviews sending {"coffeeId":"cof_001","rating":5,"comment":"Excellent"}, authenticated with the token TOKEN123.
  4. Request the Ethiopian coffees with a light roast, with the URL correctly quoted.

Solutions

Solution 1

  • (a) Method PATCH on the path /v1/coffees/cof_002.
  • (b) api.aromastore.example, indicated in the Host header (the request line never carries the domain).
  • (c) It sends application/json (Content-Type) and expects to receive application/json (Accept).
  • (d) Yes: Authorization: Bearer ... with a token.
  • (e) It partially modifies the coffee cof_002, changing only its price to €13.50. Since it is a PATCH and not a PUT, the remaining fields (name, origin, stock) are preserved. It is a typical internal-panel operation, and that is why it requires authentication.

Solution 2

Part Value
Scheme https
Host api.aromastore.example
Port Not stated; 443 is assumed because it is https
Path /v1/coffees
Query string ?roast=light&limit=10
Fragment #results

The part that does not reach the server is the fragment #results: the browser uses it locally and never sends it.

The query string modulates the query: the resource is still the collection of coffees (/v1/coffees), but we are asking for a filtered and limited subset. It is not a different resource; it is the same collection seen through other criteria. That is why filters go in the query string and not in the path.

Solution 3

# 1. Get a coffee, showing the response headers
curl -i https://api.aromastore.example/v1/coffees/cof_001

# 2. Check existence without downloading the body (HEAD request)
curl -I https://api.aromastore.example/v1/coffees/cof_999
# It would return 404 Not Found in the status line, with no body

# 3. Create an authenticated review
curl -X POST https://api.aromastore.example/v1/reviews \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN123" \
  -d '{"coffeeId":"cof_001","rating":5,"comment":"Excellent"}'

# 4. Filter coffees (URL quoted because of the &)
curl "https://api.aromastore.example/v1/coffees?origin=Ethiopia&roast=light"

Frequent mistakes in this exercise: forgetting Content-Type in point 3 (the server would not interpret the JSON), using -X GET with -I (they are incompatible: -I already implies HEAD) and not quoting the URL in point 4.

Conclusion

HTTP is the ground on which everything else is built. You now know how to read a raw request and response, distinguish their three parts, break down a URL and recognise the headers that will appear in every lesson of the course. You have seen that statelessness forces every request to be self-sufficient — and at the same time is what makes scaling possible — that the code families cleanly separate blame between client and server, that HTTPS is not optional and that the protocol versions change the transport but not the model. And, above all, you now have two tools, curl -v and the DevTools, for observing what is really happening instead of guessing at it.

With this material we can tackle the module's central question. In the next lesson, Basic Principles of REST, we will see how Roy Fielding turned the properties of the web into six architectural constraints, what exactly a resource, an identifier and a representation are, and why many APIs advertised as REST are not entirely so.

REST API Course: Principles of Designing and Developing RESTful APIs

Module 1: Introduction to RESTful APIs

Module 2: Designing RESTful APIs

Module 3: Building RESTful APIs

Module 4: Best Practices and Security

Module 5: Tools and Frameworks

Module 6: Case Studies and Projects

© Copyright 2026. All rights reserved