The conversation between browser and server, in depth: what HTTP really is, how a request becomes a response, how Flask turns Python functions into web pages — and how to test it all with curl.
← / → to navigate · EN/VI switch below
HTTP (Hypertext Transfer Protocol) runs the show. Strict turn-taking: the client speaks first, the server answers once, and the round is over.
Browser, phone app, curl… you. The only side allowed to start a conversation.
A program listening on a port (80/443). It never initiates — it only answers.
one request → one response → done · every click, refresh and API call is exactly this
Before HTTP, every client–server pair invented its own dialect. HTTP is the one dialect everyone agreed on.
Billions of clients and servers — built by different people, in different languages — agree on one set of rules. HTTP is that agreement.
A client asks, a server answers. The roles never swap — that one rule is what makes the web predictable at global scale.
Each exchange is a fresh conversation; the server forgets you the moment the response is sent. Cookies and sessions are tricks that fake memory.
Requests and responses are just formatted text. You can read them with your own eyes (curl -i) — that's what makes HTTP learnable.
Every web app you'll ever ship is these two roles talking. This course is about becoming the person who builds the right side.
Your browser, phone app, or curl. It initiates every conversation, shows you the results, and keeps nothing between visits.
Clients are impatient by design: they ask once, wait, and move on.
A program on a machine that waits on a port. It does the real work: reads databases, runs logic, builds the answer.
Servers are patient by design: they wait forever, answering thousands of times per second.
Four beats, half a second. Everything in this course is a zoom-in on one of these beats.
The browser decides 'I need that address' and builds a request: URL, method (GET), headers — a small text message.
The request rides the network to the right machine, the right port. (TLS encrypts the trip — a later topic.)
The server hands the request to your app. The app does the job: reads data, runs logic, builds a page or JSON.
The response rides back. The browser parses the HTML, applies the CSS, runs the JS, paints. Done — until the next click.
The rules for how a client asks and a server answers: anatomy of the request and the response, methods, status codes, headers, redirects.
Everything a client can say, packed into one message. No more, no less.
The address being called: host (which server) + path (what on it) + optional query (?sort=new). It's the 'who to' line of the message.
The verb: GET, POST, PUT, PATCH, DELETE. It states your intent so the server knows what to do before reading anything else.
Metadata lines, one fact per line: who's asking (User-Agent), what format the body is in (Content-Type), who you claim to be (Authorization).
The actual content being sent: form fields, JSON, a file. Optional — GET has none; POST and PUT usually carry one.
This is literally what travels between client and server. Left: the request (method + URL + headers + body). Right: the response (status + headers + body).
GET /users/42?sort=new HTTP/1.1
Host: api.shop.com
Content-Type: application/json
{"name": "Ada"}
the client's message — method, URL, who's asking, and the payload
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 46
{"id": 42, "name": "Ada", "posts": 3}
the server's reply — what happened (200), in what format, and the data
Every URL is five jobs in one line. Browsers and servers split it apart the same way every time.
https://api.shop.com:8443/users/42?sort=new&page=2
scheme https how to talk
host api.shop.com which machine answers
port :8443 which door (80 and 443 are the defaults)
path /users/42 which resource
query ?sort=new... options, as key=value pairs
Most of the time the port is hidden — :80 for http, :443 for https are implied.
Mirror image of the request — but it starts with a verdict: the status code.
First line: HTTP/1.1 200 OK. The 3-digit code is the headline — success, redirect or error — before a single byte of content.
Metadata about the content: its format (Content-Type), its size (Content-Length), cache hints. Each line is 'Name: value', like request headers.
The actual content: an HTML page, JSON, an image — or nothing. Everything after the blank line that separates it from the headers.
The method is a promise about intent. You and the server can rely on it: GET never changes anything, POST creates, DELETE removes.
| Method | It means | Example |
|---|---|---|
| GET | Read — never changes anything | GET /users/42 |
| POST | Create, or submit something | POST /users |
| PUT | Replace the whole resource | PUT /users/42 |
| PATCH | Tweak part of a resource | PATCH /users/42 |
| DELETE | Remove a resource | DELETE /users/42 |
Pick by intent, not by habit. The method tells caches, browsers and other apps how safe it is to touch your endpoint.
GET means 'show me', never 'change anything'. Browsers, caches and search engines rely on it — so GET must be safe to repeat.
POST is for sending: forms, new records, file uploads. It has a body, and repeating it may create duplicates — that's allowed.
PUT replaces the whole resource; PATCH tweaks part of it. In practice: most apps just use POST for 'write' and keep it simple.
DELETE removes a resource. The response is a status code (200 or 204), not a page — the client checks the code, not the pixels.
Every response starts with a 3-digit number. The first digit tells the whole story at a glance.
| 2xx | It means | Key codes |
|---|---|---|
| 2xx | Success — it worked | 200 OK · 201 Created |
| 3xx | Redirect — go look somewhere else | 301 moved · 302 see other |
| 4xx | Client error — your side's fault (politely put) | 400 bad · 401 not authed · 403 denied · 404 not found |
| 5xx | Server error — our side's fault | 500 internal error |
Memorize the pattern, not the list: 2xx done, 3xx go elsewhere, 4xx your problem, 5xx ours.
| Code | It means | What you do |
|---|---|---|
| 200 OK | Everything is fine; here's the data | Nothing — it worked |
| 201 Created | A new resource was born (your POST worked) | Keep its Location header — the new thing's address |
| 301 Moved Permanently | This address changed forever | Browsers update their records; fix the old link |
| 302 Found | Go look at this other URL — just this once | The browser follows automatically; you never see it |
| 400 Bad Request | Your request was malformed — I can't even parse it | Check your URL, body, headers |
| 401 Unauthorized | Who are you? I need credentials | Log in / add the Authorization header |
| 403 Forbidden | I know who you are — and no | Permissions issue; ask an admin |
| 404 Not Found | No such thing exists at that address | Check the URL — a typo, or it was deleted |
| 500 Internal Error | Our side crashed — not your fault | Check the server logs; the error page points at the line |
The most confused pair in HTTP. 401 is an identity problem; 403 is a permission problem.
You didn't show credentials, or they're wrong. The server is asking you to identify yourself.
Fix: log in, add the token. After that, the door may open.
Credentials were fine — but that account doesn't have permission for this resource.
Fix: nobody's code is wrong; it's a permissions problem.
There are ~100 headers in total. These four answer the questions that come up most often.
Which server you mean — mandatory, because virtual hosts put many sites on one IP.
The format of the body: text/html, application/json… The server's first clue about how to read your body.
A business card: which browser, app or curl is asking. Used for analytics and compatibility.
Your credentials, usually a Bearer token. This is how APIs let you in without a login page.
A redirect isn't an error — it's a signpost. You never write it, you just watch it happen.
GET /old-page. The server knows it moved — but it doesn't send the page, it sends a signpost.
301 + a Location header: /new-page. One tiny response, no content — just 'go there instead'.
It re-requests the new URL, gets 200, and shows the page. From your seat, it felt like one click.
Strip away the marketing: a web server is a program looping through four steps, thousands of times per second.
It waits on a port (80 for HTTP, 443 for HTTPS) for incoming connections — a phone left off the hook.
It decodes the bytes into method, URL, headers, body — the four parts from earlier.
It hands the request to your application (Flask, in this course), which decides what to do — usually: read some data, build a page.
It wraps the result in status + headers + body and ships it back. Then back to step 1.
One machine runs many services; the port picks the door. Production answers at 80/443; your Flask dev server usually opens 5000.
The web server speaks bytes; you think in Python. Flask is the thin layer in between — a route is a promise: 'when this URL + method arrives, run this function'.
A framework is scaffolding you don't have to build. Flask is the lightest popular scaffolding for Python web apps.
Deliberately small: routing, request parsing, responses. Databases, admin panels, auth — you bolt on only what you need.
You write plain Python functions; Flask handles the ugly part — bytes, headers, the network. A thin layer, not a platform.
Each @app.route registers one promise: 'when this method hits this URL, run this function'. The rest is bookkeeping.
flask run gives you a working server for development. It's not production-grade — that's what nginx is for (next topic).
This is a complete, runnable web app. Two routes: one for humans (HTML), one for programs (JSON).
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return "<h1>Hello, shipper!</h1>"
@app.route("/api/hello")
def api_hello():
return jsonify(name="shipper", status="alive")
Run it with flask run, open http://127.0.0.1:5000 — that's the entire server side of a web app.
That's the whole loop: Python on your machine answering HTTP on a port. You can test it before a human ever sees it.
Once you know the promise, routes are just sign-up forms. Four details worth knowing.
@app.route("/about") registers the function below for GET /about. The function's name can be anything — Flask doesn't care.
Routes default to GET (and HEAD). A write endpoint declares it: @app.route("/users", methods=["POST"]). Wrong method → 405.
Return a string → HTML page. Return jsonify(...) → JSON. Return (body, 404) → you control the status code.
/users/<int:uid> binds the 42 in /users/42 to the argument uid — typed, right in the address.
Same app, three return styles. The return value decides what the client receives — and at which status code.
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def page():
return "<h1>Hello, shipper!</h1>" # -> text/html
@app.route("/api/hello")
def api():
return jsonify(name="shipper") # -> application/json
@app.route("/missing")
def gone():
return "not here", 404 # -> you pick the status
One rule to remember: whatever the function returns, Flask wraps into status + headers + body.
One app usually does both: HTML routes for people, /api routes for programs.
The browser parses it, styles it, paints it. A human is the end consumer; the page is the product.
Test it with a browser click. If the pixels are right, it works.
A machine reads it: another app, a phone, a frontend framework. Keys and values, no decoration.
Test it with curl -i. If the code is 200 and the JSON parses, it works.
Clients send data in different shapes; Flask exposes each one in a different place. Learn where to look and the syntax follows.
from flask import request
@app.route("/users/<int:uid>") # 1. URL parameter
def show_user(uid):
return jsonify(uid=uid)
@app.route("/search")
def search():
q = request.args.get("q") # 2. query string: /search?q=shoes
name = request.form.get("name") # 3. form field (login, checkout...)
body = request.json # 4. JSON body (from an API client)
One app, four ways in. <uid> comes from the URL; the rest live on the request object.
Matching the channel to the job is the whole design decision behind every API.
| Channel | Lands in Flask | Example | Use for |
|---|---|---|---|
| URL parameter | <int:uid> in the route | /users/42 | Which resource — part of the address |
| Query string | request.args | /search?q=shoes | Filters, options, pagination |
| Form body | request.form | <form> posts | Login, checkout — the classic browser form |
| JSON body | request.json | curl -d '{...}' | APIs talking to APIs — the modern default |
Query values arrive as strings — even '42'. Set defaults, and convert types yourself.
@app.route("/search")
def search():
q = request.args.get("q", "") # ?q=shoes -> "shoes", else ""
page = request.args.get("page", 1, type=int) # ?page=2 -> 2 (an int)
if not q:
return "nothing to search", 400
return jsonify(q=q, page=page)
get(key, default, type=...) is the 90% solution for query strings.
Four stops between 'bytes arrive' and 'bytes leave'. Knowing the route makes debugging 10x faster.
The web server (or Flask's dev server) gets the raw HTTP bytes and hands them to Flask.
It walks your registered promises: method + URL. No match? Instant 404 — your code never runs.
The view function reads the input (args, form, json), does the work — often: fetch data — and returns a value.
The return value becomes status + headers + body, and rides back to the client. The round trip is over.
Flask's dev server shows the full traceback on 500s — the last line usually names the exact bug. Fix, save, curl again.
A correct app fails politely: the right code tells the client exactly what to try next.
return 'not found', 404 — the second value sets the status. Stop answering 200 for everything.
400: the request itself was malformed. 404: the request was fine, the thing doesn't exist.
An API client branches on the status code. Your 4xx and 5xx are instructions, not decorations.
A JSON API is invisible to a browser's way of seeing. curl is the terminal's HTTP client: it performs a request and prints the raw response.
curl is the browser's blunt, honest cousin: it shows the plumbing the browser hides.
A browser shows you the painted result and swallows the status code, headers and redirects. curl shows every byte.
A JSON endpoint opened in a browser is a dead end. curl is the native habitat of an API.
Pick the method, the headers, the body, the exact URL. What the browser does for you on a click, you do deliberately.
curl lives in shell loops, Makefiles and CI pipelines. If a test can be typed, it can be automated.
Success, data, filtered data, failure. This is what testing an API feels like.
curl's default is: GET a URL and print the body. The flags bend it into a full API client.
| Flag | What it does | Example |
|---|---|---|
| -X POST | Switch the method | curl -X POST http://…/users |
| -H 'K: V' | Add a header | -H 'Content-Type: application/json' |
| -d data | Attach a body | -d '{"name":"Ada"}' |
| -i | Show status line + headers | curl -i http://…/ |
| -v | Verbose: the whole connection story | curl -v http://…/ |
| -o file | Save the body to a file instead of the screen | curl -o page.html http://…/ |
Method + header + body + URL. This one pattern covers most of the 'send data to my API' work.
# the classic "create a record" one-liner
curl -X POST \
-H 'Content-Type: application/json' \
-d '{"name": "Ada"}' \
http://127.0.0.1:5000/users
# ...and check what the server actually said
curl -i -X POST \
-H 'Content-Type: application/json' \
-d '{"name": "Ada"}' \
http://127.0.0.1:5000/users
With -i you should see HTTP/1.1 201 CREATED — proof the record was born.
Lines starting with > are yours; lines starting with < are the server's. This is the whole protocol, live.
Three roles, one pipeline — and the next topic (nginx) plugs in right here.
Browser for humans, curl for you. They start every conversation.
Next topic. Terminates HTTPS, serves static files, forwards real requests onward.
Your code: routes, data, decisions. Builds the response and sends it back.
clients talk to the door · the door talks to the app · the app talks to the world — all in plain HTTP
Every web app you'll ever ship is a loop: listen, parse, run, answer. Flask writes the answers; curl proves they arrive.