1. Introduction
When you first hear “networking” and HTTP, it can feel like requests are flying around somewhere in the fog, servers “return something,” and the browser “somehow” shows the result. In practice, it’s much more down-to-earth (and that’s actually nicer): HTTP is an agreement about exactly how two parties exchange messages. Not “how to connect to Wi‑Fi,” not “how to lay a cable across an ocean,” but precisely: what text/bytes we send and what we consider a valid response.
The word “contract” fits well here. A contract is when you don’t try to guess the server’s mood—you follow the rules: “I send this, in this form, with these parameters; you respond like that, with this status and body.” And if you can read that contract, network code turns from shamanism into a normal engineering task: “build request → send → parse response.”
Mini analogy
You can think of HTTP as ordering in a cafe: you say what you want (method), which exact menu item (URL/endpoint), any preferences (headers), and sometimes order details (body). The waiter brings you a receipt with a status (success or error) and, possibly, the dish (response body). If instead of “cappuccino” you say “water, but make it like coffee,” you’ll get something weird—about as weird as it is for the server when you mix up the format and meaning of a request.
2. Request and response: the core HTTP pair
If you take one idea from this lecture, let it be this one: HTTP is always the pair “request → response”. You send a request (request), and the server returns a response (response). Even if the response is an “error,” it’s still a response. This is crucial: in HTTP, an error is often expressed not as an exception on your side, but as a normal response with a status that says “I understood you, but…”.
Below is a diagram that should become as familiar in your head as “called a function → got a result”:
flowchart LR
C["Client (your program)"] -->|HTTP request| S["Server (API)"]
S -->|HTTP response| C
What it looks like “raw”
An HTTP message is a header section (lines) + sometimes a body. No need to panic: you don’t have to hand-write this all the time, but understanding the structure is mandatory.
Example request (illustrative):
GET /api/expenses?limit=3 HTTP/1.1
Host: example.com
Accept: application/json
Example response (illustrative):
HTTP/1.1 200 OK
Content-Type: application/json
[{"id":1,"title":"Coffee","amount":3.5}]
Notice something important: in the request there is “what I want” (GET) and “where” (/api/expenses?...). In the response there is “how it ended” (200 OK) and “what we got” (the body).
3. URL and endpoint: where we go and what we ask for
URL is often confused with a “link,” but for a client a URL is the address of a resource. An endpoint is usually a “specific API path,” i.e., the part of the URL that describes which exact function/resource you’re calling on the server. In developer talk, an endpoint is not philosophy—it’s shorthand for “which path we hit.”
To read a URL confidently, it helps to see it as a set of parts. For example:
https://api.example.com:443/v1/users/42?verbose=true#fragment
| Part | Example | Why it matters |
|---|---|---|
| scheme | |
which protocol (usually or ) |
| host | |
which server (domain) |
| port | |
which port (often hidden; for https it’s typically 443) |
| path | |
which resource/endpoint |
| query | |
request parameters “in the string” |
| fragment | |
usually not used for HTTP APIs (more of a browser thing) |
Kotlin mini-example: break a URL into parts
We’re not going to the network yet; we’re just training our brain and strings.
fun main() {
val url = "https://api.example.com/v1/users/42?verbose=true"
val scheme = url.substringBefore("://")
val rest = url.substringAfter("://")
println("scheme=$scheme") // scheme=https
println("rest=$rest") // rest=api.example.com/v1/users/42?verbose=true
}
Yes, this is “parsing on your knee.” But it shows an important idea: a URL is a string with structure, and later tools will do the same thing—just safer and more conveniently.
4. HTTP method: what exactly we do
When you see GET or POST, those aren’t “just words”—they’re part of the contract: the method tells the server your intent. For the client side today, two methods are enough, because they cover the lion’s share of real-world scenarios.
GET means: “give me a representation of the resource.” Usually this is reading data. Ideally, GET should not change the server state (i.e., it should be “safe”), and typically there is no request body in GET.
POST means: “accept data” or “create something based on this data.” Often POST is used to create a new record, start an operation, or submit a form. POST usually comes with a body, and very often that body is JSON.
Table to lock it in
| Method | Typical meaning | Request body? |
|---|---|---|
|
fetch data | usually no |
|
send data / create | usually yes |
Kotlin mini-example: method + URL as a meaningful pair
Still no networking—just capturing the idea: a request starts with “what we do” + “where.”
fun main() {
val method = "GET"
val url = "https://api.example.com/v1/expenses?limit=10"
println("$method $url") // GET https://api.example.com/v1/expenses?limit=10
}
5. Request formats: headers and body
The method and URL answer the questions “what” and “where.” Then comes the practical part of the contract: in what form we communicate, and which exact data we transmit.
Headers
Headers in HTTP are additional “key: value” fields that help both sides agree on details. If the method and URL answer “what” and “where,” headers answer “in what form,” “with what constraints,” and “with which transfer parameters.”
It’s very important not to treat headers as “some boring service part.” In practice, headers in an API often decide everything: data format, language, versioning, authorization (though we’re intentionally not going deep into authorization today).
Two headers you should remember right now:
Accept — “which response format I want.” If you expect JSON, it’s logical to say so.
Content-Type — “what format my request body is in.” This matters when you send data (for example, JSON in POST). If you don’t specify Content-Type, the server may interpret the body incorrectly or refuse it.
Kotlin mini-example: think of headers as a Map
To understand the structure, it’s convenient to think of headers as a dictionary.
fun main() {
val headers = mapOf(
"Accept" to "application/json",
"Content-Type" to "application/json"
)
println(headers["Accept"]) // application/json
println(headers["Content-Type"]) // application/json
}
Yes, in real HTTP, headers are lines of text, key case isn’t always important, and there are nuances. But as a beginner’s model, Map<String, String> is an excellent starting point.
Body
The request or response body is the “payload.” If you do a GET, most often you send nothing in the body and receive a body in the response. If you do a POST, you often send a body (for example, JSON), and a body may also come back in the response (for example, the created object or an error message).
Key point: a body is not necessarily a string. Technically it’s a sequence of bytes. But in this course (and in most typical API scenarios) we will very often think of the body as text—most often JSON text.
Kotlin mini-example: build a JSON body as a string
We’re not serializing a model yet—we’re just showing the idea “the body contains JSON.”
fun main() {
val title = "Coffee"
val amount = 3.5
val jsonBody = """{"title":"$title","amount":$amount}"""
println(jsonBody) // {"title":"Coffee","amount":3.5}
}
Later you’ll do this via kotlinx.serialization, and it will be more reliable (no need to manually watch quotes), but the idea stays the same: the body is data in an agreed-upon format.
6. Status code: how the server reports the result
The status code is the first thing you should look at in a response. It answers: “how did the attempt end?” And the word attempt matters here. The server might dislike your request, it might crash itself, it might successfully process everything—and all of that is expressed with numbers.
You don’t need to memorize every code. It’s enough to confidently understand the classes:
• 2xx — success (the request was processed).
• 4xx — client-side error (you sent the wrong thing, to the wrong place, in the wrong format, with the wrong data).
• 5xx — server-side error (you might have done everything correctly, but the server couldn’t process the request).
Table: a few codes you see all the time
| Code | Name | Typical meaning |
|---|---|---|
|
OK | everything is fine; there is a result |
|
Created | something was created (often after POST) |
|
Bad Request | the request is invalid (format/validation) |
|
Unauthorized | not authorized (usually about tokens/login) |
|
Forbidden | access denied |
|
Not Found | resource not found |
|
Internal Server Error | error inside the server |
Kotlin mini-example: checking “is this successful?”
It’s very useful to immediately get used to the idea “success is a range.”
fun isSuccessful(status: Int): Boolean = status in 200..299
fun main() {
println(isSuccessful(200)) // true
println(isSuccessful(404)) // false
}
Psychologically, this is more important than “check for 200.” Because there are multiple successful statuses, and your code should be ready for that (201, 204, etc.).
7. Order of operations: status → branch → interpret the body
When you start writing networking code, the most common mistake is trying to immediately “parse the pretty JSON.” Your brain wants a reward: “here’s the data, here’s the list, let’s go.” But networking is unforgiving, and very often the server returns a body that looks like normal text but is, in meaning, an error. So the order of operations must be disciplined.
First you receive the response and read the status. Then you decide whether it’s success or error. Only after that do you interpret the body: either as “data” or as an “error message” (or another format, depending on the API).
It’s convenient to keep this discipline in mind as a small flowchart:
flowchart TD
A["Received response"] --> B["Checked status"]
B -->|2xx| C["Read body as data (e.g., JSON)"]
B -->|non-2xx| D["Read body as error/message"]
Kotlin mini-example: request result as a type
We’re not making a real request, but we’re modeling a well-designed result contract (you already know this from sealed class).
sealed class HttpCallResult {
data class Ok(val body: String) : HttpCallResult()
data class HttpError(val status: Int, val body: String) : HttpCallResult()
}
fun handle(status: Int, body: String): HttpCallResult =
if (status in 200..299) HttpCallResult.Ok(body)
else HttpCallResult.HttpError(status, body)
fun main() {
val r = handle(404, """{"error":"not found"}""")
println(r) // HttpError(status=404, body={"error":"not found"})
}
This feels “extra” right up until the first time you debug: a typed result forces you to not mix up success and error.
8. Contract for the learning app ExpenseTracker
So that today’s lecture isn’t only “about the internet in a vacuum,” let’s tie it to our hypothetical practical CLI app that tracks expenses (let’s call it ExpenseTracker). Until now it lived locally: input, collections, files, JSON. Now we want to imagine we have an external service that stores expenses in the cloud.
We’re not writing networking code yet. Our goal is to learn to read and design a contract: which endpoints exist, which methods, which statuses, which bodies.
Endpoint for getting the expense list
Let’s imagine the server provides an endpoint:
GET https://api.example.com/v1/expenses?limit=3
Then the “raw” interaction can be imagined like this.
Request (example):
GET /v1/expenses?limit=3 HTTP/1.1
Host: api.example.com
Accept: application/json
Response on success:
HTTP/1.1 200 OK
Content-Type: application/json
[
{"id": 1, "title": "Coffee", "amount": 3.5},
{"id": 2, "title": "Lunch", "amount": 12.0}
]
And here the contract tells us several things at once. The GET method and the absence of a request body hint: this is reading. Accept: application/json says: “we want JSON.” 200 OK says: “success.” Content-Type in the response confirms: “yes, it’s JSON.”
Endpoint for adding an expense
Now adding:
POST https://api.example.com/v1/expenses
The request body is JSON with the new expense data.
Request (example):
POST /v1/expenses HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{"title":"Coffee","amount":3.5}
Response on success (one possible variant):
HTTP/1.1 201 Created
Content-Type: application/json
{"id": 10, "title":"Coffee","amount":3.5}
A very typical pattern: the server returned 201 Created and sent back the created object with an assigned id.
Kotlin mini-example: a request data draft as a model
Let’s say we already have a model (no serialization, just structure):
data class NewExpense(
val title: String,
val amount: Double
)
fun main() {
val expense = NewExpense(title = "Coffee", amount = 3.5)
println(expense) // NewExpense(title=Coffee, amount=3.5)
}
That’s enough for today: we’re capturing that the request body is a representation of our data, usually in JSON.
9. Common mistakes when understanding the HTTP contract
Mistake #1: assuming “if a body arrived, it must be success.”
This is a very human trap: you got some text, saw curly braces, got excited, and started parsing it as data. But the server can absolutely return a JSON error with status 400 or 404. That’s why the discipline “status first, then interpret the body” saves hours of life and pounds of nerves (and nerves, as we know, are not val but rather var, and they’re very easy to ruin).
Mistake #2: mixing up the meaning of GET and POST.
Beginners sometimes use POST “because that’s what works,” or, conversely, try to send a body in GET “because it’s more convenient for me.” In practice, APIs almost always expect standard semantics: GET — read, POST — send/create. When you stick to that semantics, your code becomes predictable for other developers and for the server side, and unpredictability is better left to cats and quantum physics.
Mistake #3: ignoring formats and headers.
You can try to “just send text” and hope the server guesses. Sometimes it will. Sometimes it won’t. And sometimes it will “guess wrong”—which is the most dangerous option, because you’ll get weird errors. The Accept and Content-Type headers are your way to stop guessing and explicitly agree on the format. It’s especially important to remember Content-Type when sending a JSON body.
Mistake #4: expecting an HTTP error to be an exception.
In Kotlin you’re used to: something went wrong—catch an Exception. In HTTP the logic is different: the server can honestly return 404 as a normal response, and that’s not a “network failure,” but a result of processing the request. These are two different categories of problems: “the server responded with an error” and “we couldn’t perform the request at all.” Today we’re only noting this at the meaning level; in the next lectures you’ll learn to separate these cases in code.
Mistake #5: “forgetting” the contract and trying to guess behavior from experience.
Sometimes it feels easier to try to “call the URL somehow,” and then “we’ll see.” But the HTTP contract itself (method, URL, headers, expected statuses, body format) is what gives you stability. It makes integration repeatable: another developer can reproduce the request, you can write clear error handling, and your app will behave the same on different days of the week (even on Monday).