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
- What HTTP is and what role it plays in an API
- Anatomy of an HTTP request
- Anatomy of an HTTP response
- The request-response model and the absence of state
- The URL and its parts
- Common headers in APIs
- Overview of methods and status codes
- HTTPS and TLS: why every API is encrypted
- HTTP/1.1, HTTP/2 and HTTP/3
- Observing HTTP:
curl -vand the DevTools
- 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
- 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
Hostheader. When you typehttps://api.aromastore.example/v1/orders, the client splits that address into the two parts automatically.
- 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.
- 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_002the 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.
- 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 chainskey=valuepairs 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 writeorigin=Ethiopia, and a value such asPerúwould travel asorigin=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=lightYou 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.
- 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-TypeandAcceptare not the same thing.Content-Typedescribes what I am sending;Acceptdescribes what I want to receive. APOSTrequest usually carries both: "I am sending you JSON and I want JSON back". AGETcarries onlyAccept, because it sends no body. This mechanism is called content negotiation and is the subject of 02-05.Authorizationwith theBearerscheme 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-typeis the same asContent-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.
- 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.
GETandHEADare. 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,PUTandDELETEare;POSTis not. Hence retrying aPOST /v1/ordersafter 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.
- 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:
- Confidentiality: nobody along the way can read the content. Without TLS, the
Authorizationheader with the token travels in plain text and anyone on the same wi-fi network can copy it. - Integrity: nobody can alter the messages without it being detected.
- Authenticity: the server's certificate proves you are talking to
api.aromastore.exampleand 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
httptraffic tohttpswith a301, 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:
We will expand on all this in lesson 04-02, devoted to security.
- 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
Authorizationin 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.
- Observing HTTP:
curl -v and the DevTools
curl -v and the DevToolsYou 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.
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:
curlautomatically addedHost,User-AgentandAccept: */*("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 anETag, 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.jsonA 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:
- Filter by Fetch/XHR to see only the API calls the page makes, ignoring images and stylesheets.
- 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).
- Use Copy as cURL in the context menu: it turns any browser request into a
curlcommand 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
AcceptwithContent-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
curlwhen 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 OKfor 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 -vat 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.
Exercise 3: build requests with curl
Write the curl commands to:
- Get the coffee
cof_001, showing the response headers. - Check whether the resource
/v1/coffees/cof_999exists without downloading the body. - Create a review with
POST /v1/reviewssending{"coffeeId":"cof_001","rating":5,"comment":"Excellent"}, authenticated with the tokenTOKEN123. - Request the Ethiopian coffees with a light roast, with the URL correctly quoted.
Solutions
Solution 1
- (a) Method
PATCHon the path/v1/coffees/cof_002. - (b)
api.aromastore.example, indicated in theHostheader (the request line never carries the domain). - (c) It sends
application/json(Content-Type) and expects to receiveapplication/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 aPATCHand not aPUT, 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
- What Is an API?
- History and Evolution of APIs
- HTTP Fundamentals for APIs
- Basic Principles of REST
- The Richardson Maturity Model and HATEOAS
- REST vs. SOAP
- REST Compared with GraphQL, gRPC and Webhooks
Module 2: Designing RESTful APIs
- RESTful API Design Principles
- Resources and URIs
- HTTP Methods
- HTTP Status Codes
- Representations, Headers and Content Negotiation
- Filtering, Sorting, Pagination and Search
- API Versioning
- API Documentation
Module 3: Building RESTful APIs
- Setting Up the Development Environment
- Building a Basic Server
- Handling Requests and Responses
- Input Data Validation
- Persistence and the Data Access Layer
- Authentication and Authorisation
- Error Handling
- Testing and Validation
Module 4: Best Practices and Security
- API Design Best Practices
- Security in RESTful APIs
- OAuth 2.0 and OpenID Connect in Practice
- Rate Limiting and Throttling
- CORS and Security Policies
- HTTP Caching and Performance
- Observability: Logs, Metrics and Traces
Module 5: Tools and Frameworks
- Postman for API Testing
- Swagger and OpenAPI for Documentation
- Popular Frameworks for RESTful APIs
- Contracts, Mocks and Automated API Testing
- Continuous Integration and Deployment
- API Gateways and Developer Portals
