◐ ai-teacher

🐍 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 (200 ok, 404 not 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.

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:

  1. The data — the single copy of the truth, in a database.
  2. The rules — prices, limits, who may do what. A rule checked only in the app is a suggestion.
  3. 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

Words to know

BackendThe server-side program that receives requests, applies the rules, touches the data and sends responses.
RequestWhat a client sends: an HTTP method, a path (with optional query string), headers and an optional body.
ResponseWhat the server sends back: a status code, headers and an optional body (usually JSON).
RouteA rule mapping a method and path to one handler, e.g. GET /price → get_price.
HandlerThe function that runs for one route; takes the request, returns the response.
Status codeA three-digit number at the start of the response: 2xx success, 4xx client's fault, 5xx server's fault.
Stateless handlerA handler that keeps nothing in memory between requests; anything that must persist goes to a database.

In two minutes

Test yourself

1. What are the parts of an HTTP request?

  1. A status code, headers and a body
  2. A method, a path, headers and an optional body
  3. A handler, a router and a database
  4. 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?

  1. Because JSON can't carry numbers
  2. Because looking it up is faster
  3. Because HTTP strips numbers out of POST bodies
  4. 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?

  1. It matches the request's method and path against the registered routes
  2. It tries every handler until one doesn't raise
  3. It reads the handler name from a request header
  4. 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?

  1. Python lists aren't thread-safe
  2. The browser caches old responses
  3. Each worker process has its own separate list, and each refresh may hit a different worker
  4. 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?

  1. The client gets 404 Not Found
  2. The server process exits
  3. The client gets 500 Internal Server Error and the server keeps running
  4. 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"}?

  1. Nothing, as long as the client reads the body
  2. Retries, monitoring, caches and load balancers read the status code, so they all record a success
  3. JSON bodies aren't allowed with 200
  4. 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.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →