🐍 Backend with Python · Chapter 1
What a backend is: request, handler, response
Every app you use is a thin screen in front of a program you never see: it reads one request, runs one function, and writes one response, millions of times a day.
Study this lesson free in the app →
The basics
A backend is a program running on a server that waits for messages from apps and browsers, does the work they ask for, and replies. Every exchange has the same three parts: a request comes in, a handler (a plain function) runs, and a response goes back.
The problem it solves: the phone in your hand can't be trusted with the data, the rules or the secrets, so one program on a server owns them and every client has to ask it.
The mental model: a railway booking counter. You hand over a filled form (request). The clerk who handles that kind of form (handler) checks it against the chart and the rules. You get a ticket or a refusal slip with a reason (response). The clerk does not remember you afterwards: next time you bring your PNR.
Words you must know 4
- Request
- method (
GET,POST…), path (/price), headers, and an optional body. - Route
- the rule "this method + this path goes to this function". The router looks it up.
- Handler
- the function that does the work for one route. Request in, response out.
- Response
- a status code (
200ok,404not found…), headers, and a body, usually JSON.
Explained
You tap Pay ₹180 in a food app. Your phone does not move any money. It sends about 300 bytes of text to a server: who you are, what you want, how much.
On the server, one Python function reads that text, checks your balance and the restaurant, writes a row to a database, and sends back a few lines that start with 201.
That round trip — request, handler, response — is the whole job. Everything else in this book is making it correct, fast and boring under load.
The railway reservation counter
Before apps, you booked a train at a counter in Secunderabad station.
- You fill a form: train number, date, class, names. That is the request.
- Window 3 does sleeper bookings, window 5 does cancellations. Picking the window is routing.
- The clerk at that window checks the form, checks the chart, applies the rules. That clerk is the handler.
- You get back a ticket, or a slip saying "no berths — waitlisted 14". That is the response, and the first word tells you the outcome before you read the rest.
Two things about the clerk matter later. He doesn't trust you: you say "I'm a senior citizen", he asks for proof. And he doesn't remember you: tomorrow you bring your PNR, because the truth lives in the chart, not in his head.
Precisely: the parts and who owns what
| Part | What it is | Example | | --- | --- | --- | | Client | whatever sends the request | phone app, browser, another service | | Server process | a program listening on a port | uvicorn on port 8000 | | Request | method + path + headers + body | GET /price?item=chai | | Router | maps (method, path) → function | ("GET", "/price") → get_price | | Handler | your function | reads input, applies rules, touches the DB | | Response | status + headers + body | 200, Content-Type: application/json, {"price": 15} |
The backend owns three things the client must never be trusted with:
- The data — the single copy of the truth, in a database.
- The rules — prices, limits, who may do what. A rule checked only in the app is a suggestion.
- The secrets — database passwords, payment-gateway keys.
Each request is handled on its own. Anything the server must remember between requests goes to a database or a cache, not into a Python variable.
One request, end to end
Steps 1–3 and 6 are plumbing a framework does for you. Steps 4 and 5 are where your bugs and your design live.
A backend with nothing but the standard library
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
MENU = {"chai": 15, "samosa": 20}
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
if self.path == "/menu":
body = json.dumps(MENU).encode()
self.send_response(200)
else:
body = b'{"error": "not found"}'
self.send_response(404)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()Send it GET /menu HTTP/1.1 over a raw socket and this is every byte that comes back:
HTTP/1.1 200 OK
Server: BaseHTTP/0.6 Python/3.14.6
Date: Sun, 08 Nov 2026 10:43:14 GMT
Content-Type: application/json
Content-Length: 26
{"chai": 15, "samosa": 20}Status line, headers, a blank line, the body. That's HTTP. Ask for /menu/ instead and you get HTTP/1.1 404 Not Found with {"error": "not found"}: the router here is one if, and /menu/ is a different string.
Watch a request find its handler
Running it prints {'status': 200, 'body': {'item': 'chai', 'price': 15}}. FastAPI is this same loop, with the dict-building, parsing and checking done for you.
Predict: the request with nothing in it
Same toy backend as the trace. A buggy app sends:
app({"method": "GET", "path": "/price", "query": {}})Mistakes every first backend makes
- Trusting the client's numbers. If the app sends
"price": 1, the server must ignore it and look the price up itself. Anything from the client is a claim, not a fact. - Keeping state in a Python variable. A global
orders = []vanishes on restart and is different in each worker process. State goes in a database. - Answering
200with{"error": "..."}. Monitoring, retries and caches read the status code, not your body. Failures need 4xx/5xx. - Slow work inside the handler. Sending an email or rendering a PDF while the user waits for 6 seconds. Hand it to a queue (be-010).
- Letting exceptions escape. An unhandled
KeyErrorbecomes a bare500 Internal Server Error; the client learns nothing and on-call gets paged.
Words to know
| Backend | The server-side program that receives requests, applies the rules, touches the data and sends responses. |
|---|---|
| Request | What a client sends: an HTTP method, a path (with optional query string), headers and an optional body. |
| Response | What the server sends back: a status code, headers and an optional body (usually JSON). |
| Route | A rule mapping a method and path to one handler, e.g. GET /price → get_price. |
| Handler | The function that runs for one route; takes the request, returns the response. |
| Status code | A three-digit number at the start of the response: 2xx success, 4xx client's fault, 5xx server's fault. |
| Stateless handler | A handler that keeps nothing in memory between requests; anything that must persist goes to a database. |
In two minutes
- A backend is a long-running program that answers requests: request in, handler runs, response out.
- A request is a method, a path, headers and an optional body; a response is a status code, headers and a body.
- The router maps (method, path) to one handler function; no match means 404 (or 405 for a wrong method).
- The backend owns the data, the rules and the secrets, because the client can be modified by anyone.
- Handlers treat every request independently; state lives in a database, never in a Python global.
- The status code is the outcome that machines read; a failure dressed as 200 breaks retries and monitoring.
- Frameworks like FastAPI do the parsing, routing and serialising; your job is the handler and its rules.
Test yourself
1. What are the parts of an HTTP request?
- A status code, headers and a body
- A method, a path, headers and an optional body
- A handler, a router and a database
- A URL and a JSON object, always
Show the answer
A method, a path, headers and an optional body — A request carries a method and path, plus headers and maybe a body. A status code belongs to the response. GET requests usually have no body at all, so 'always a JSON object' is wrong.
2. Why must the server look up the price itself instead of using the price the app sends?
- Because JSON can't carry numbers
- Because looking it up is faster
- Because HTTP strips numbers out of POST bodies
- Because anyone can modify the app or send requests by hand, so client data is only a claim
Show the answer
Because anyone can modify the app or send requests by hand, so client data is only a claim — A request can be crafted with curl in ten seconds. Rules and prices enforced only in the client are suggestions. Speed is irrelevant here; correctness is the point.
3. How does a router decide which handler runs?
- It matches the request's method and path against the registered routes
- It tries every handler until one doesn't raise
- It reads the handler name from a request header
- It picks the handler defined first in the file
Show the answer
It matches the request's method and path against the registered routes — A route is the pair (method, path). The same path can have different handlers for GET and POST. Nothing in the request names your Python function.
4. A team keeps orders = [] as a module-level list and runs the service with 4 worker processes. Why does the order count on their dashboard jump around between refreshes?
- Python lists aren't thread-safe
- The browser caches old responses
- Each worker process has its own separate list, and each refresh may hit a different worker
- JSON can't serialise long lists
Show the answer
Each worker process has its own separate list, and each refresh may hit a different worker — Processes don't share memory. Four workers means four different lists, each seeing about a quarter of the orders. Thread-safety is a real issue, but not the cause here: these are separate processes.
5. What happens when a handler raises a KeyError that nothing catches?
- The client gets 404 Not Found
- The server process exits
- The client gets 500 Internal Server Error and the server keeps running
- The client gets 200 with an empty body
Show the answer
The client gets 500 Internal Server Error and the server keeps running — Frameworks catch unhandled exceptions at the edge and answer 500, then keep serving other requests. It's a 500, not a 404, even if the missing key was the item the user asked for: the framework can't guess your intent. Trap: 404 feels right, but you have to raise it yourself.
6. Trap: what's wrong with returning status 200 and body {"error": "payment failed"}?
- Nothing, as long as the client reads the body
- Retries, monitoring, caches and load balancers read the status code, so they all record a success
- JSON bodies aren't allowed with 200
- It is slower than a 500
Show the answer
Retries, monitoring, caches and load balancers read the status code, so they all record a success — The tempting answer is 'nothing, the app checks the body'. But your error-rate dashboards, retry logic and alerting see a 200 and count a success. Use the status code for the outcome and the body for the details.
There are 9 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.
Start this lesson →