During an IPL match, the Cricbuzz or ESPNcricinfo app on your phone refreshes the score every few seconds without you touching anything. Multiply that by the tens of millions of phones watching the same match, and you get a staggering number: a server somewhere is answering something on the order of tens of thousands of "what's the score now?" requests every second, for the exact same match, from devices that have no persistent connection to each other or to the server between refreshes. Each phone asks its question, gets an answer, and forgets the conversation ever happened. Five seconds later it asks again, from scratch, as if for the first time. That "ask fresh every time" pattern is not an implementation shortcut — it is the central design idea behind REST, and it is exactly what you build when you write a Flask API. This chapter builds a miniature version of that score service, traces a request through it line by line, and uses it to ground two things Grade 11 needs formally: what REST actually constrains, and how a web framework turns an HTTP request into Python code and back into JSON.
What REST Actually Constrains
REST — Representational State Transfer — is not a protocol, a library, or a file format. It is an architectural style, described by Roy Fielding in his 2000 PhD dissertation at UC Irvine, "Architectural Styles and the Design of Network-based Software Architectures." Fielding was trying to explain why the web itself scaled so well, and he distilled it into a small set of constraints. Two of them matter most for the API you are about to write.
The first is resources identified by URIs, manipulated through a uniform interface. In REST, everything the API exposes is a "resource" — a match, a list of matches, a score — and each resource gets its own URL. You do not design endpoints like /getMatchScore or /updateScore; the verb lives in the HTTP method (GET to read, POST to create or act, PUT to replace, DELETE to remove), not in the URL. The URL names a noun: /matches/101/score. This is why REST API design is sometimes described as "nouns in the URL, verbs in the HTTP method" — it keeps the interface uniform across every resource in the system instead of inventing a new calling convention per feature.
The second is statelessness: every request must carry everything the server needs to process it. The server does not remember that you asked for match 101 thirty seconds ago; it does not keep a running conversation with your specific phone. This is precisely the cricket-app behavior from the opening paragraph — each poll is a complete, independent transaction. Statelessness is what lets a server handle millions of clients with a fixed, small amount of per-request work: there is no per-client session object growing in memory, no conversational context to keep synchronized across the load balancer's many backend machines. Any of the app's thousands of servers can answer any phone's request, because no server holds private memory of a specific client's history.
Flask, Werkzeug, and How a URL Becomes a Function Call
Flask is a WSGI micro-framework: it does not include a database layer, a templating requirement, or an opinionated project structure. What it gives you is routing and request/response handling, built on a lower-level library called Werkzeug. When you write @app.route("/matches/<int:match_id>/score"), Flask registers that pattern with Werkzeug's URL map at startup. Werkzeug compiles each registered rule into a matching pattern with typed placeholders — <int:match_id> means "match a segment that converts cleanly to an integer, and pass it to the view function as the Python int match_id." When a request comes in, Werkzeug checks the incoming path against these compiled rules and, on a match, dispatches to the corresponding Python function — the "view function" — passing along any captured path variables as arguments.
This typed-converter detail matters more than it looks. If a client requests /matches/abc/score, the string "abc" fails the int conversion, so the rule simply does not match — Werkzeug never calls your function with a bad argument. Instead, since no rule matches the path, it falls through to the framework's default 404 handler. Type checking happens at the routing layer, before your code runs, not as a runtime crash inside it.
Building the Score API
Here is a small but complete Flask app modeling the cricket-score scenario: a GET endpoint to list live matches, a GET endpoint to read a match's current score, and a POST endpoint to add runs to it.
from flask import Flask, request, jsonify
app = Flask(__name__)
matches = {
101: {"team_a": "MI", "team_b": "CSK", "overs": 18.4,
"score_a": 176, "wickets_a": 4},
102: {"team_a": "RCB", "team_b": "KKR", "overs": 12.2,
"score_a": 98, "wickets_a": 2},
}
@app.route("/matches", methods=["GET"])
def list_matches():
return jsonify(list(matches.keys())), 200
@app.route("/matches/<int:match_id>/score", methods=["GET"])
def get_score(match_id):
match = matches.get(match_id)
if match is None:
return jsonify({"error": "match not found"}), 404
return jsonify(match), 200
@app.route("/matches/<int:match_id>/score", methods=["POST"])
def update_score(match_id):
if match_id not in matches:
return jsonify({"error": "match not found"}), 404
payload = request.get_json(silent=True)
if payload is None or "runs" not in payload:
return jsonify({"error": "runs field required"}), 400
matches[match_id]["score_a"] += payload["runs"]
return jsonify(matches[match_id]), 200
if __name__ == "__main__":
app.run(debug=True)
A few choices here are deliberate, not decorative. request.get_json(silent=True) parses the request body as JSON but returns None instead of raising an exception if the body is missing, malformed, or sent with the wrong Content-Type header — this is what makes the following if payload is None check meaningful rather than dead code. jsonify(...) converts a Python dict or list into a proper JSON HTTP response with the correct Content-Type: application/json header set automatically; returning a raw Python dict without it would rely on Flask's newer implicit conversion, which is worth knowing exists but is less explicit about what is happening. Every view function returns a tuple of (body, status_code) — Flask accepts this pattern directly and uses it to set the HTTP status line.
You would run this with python app.py, then test it from a second terminal:
curl http://127.0.0.1:5000/matches
curl http://127.0.0.1:5000/matches/101/score
curl -X POST http://127.0.0.1:5000/matches/101/score \
-H "Content-Type: application/json" \
-d '{"runs": 6}'
Tracing a Request End to End
Reading code is not the same as knowing what it does. Trace the POST request above, step by step, against the running app.
1. The client sends POST /matches/101/score with header Content-Type: application/json and body {"runs": 6}.
2. Werkzeug's URL map checks this path against registered rules. /matches/<int:match_id>/score matches, with "101" converting cleanly to the Python int 101. Because this request's method is POST, Werkzeug dispatches specifically to update_score, not get_score — Flask keeps separate handlers per method even when the URL pattern is identical.
3. Flask calls update_score(match_id=101).
4. match_id not in matches evaluates to 101 not in {101: ..., 102: ...}, which is False, so execution continues past the first guard.
5. request.get_json(silent=True) parses the body into the Python dict {"runs": 6} and assigns it to payload.
6. payload is None is False; "runs" not in payload is False (the key is present) — so the combined or condition is False, and the 400 branch is skipped.
7. matches[101]["score_a"] += payload["runs"] executes as matches[101]["score_a"] = 176 + 6, mutating the dictionary in place to 182.
8. jsonify(matches[101]) serializes the now-updated dictionary. Flask's jsonify() sorts object keys alphabetically by default, so the actual response body is {"overs": 18.4, "score_a": 182, "team_a": "MI", "team_b": "CSK", "wickets_a": 4}, paired with status 200.
9. The client receives HTTP 200 with that exact JSON body.
Note what did not happen: nothing about this request depended on the GET request the client might have made five seconds earlier. Steps 4 through 6 re-derive everything needed from this request's own body and path — that is statelessness in code, not just in definition.
Anatomy of the Request, Visually
Where the Data Actually Lives — and What It Costs to Find It
The matches dictionary is a hash table. Python's dict hashes the key — here, an integer match_id — into a bucket index, giving average-case O(1) lookup, insertion, and update, regardless of how many matches are stored. Worst case, if many keys collided into the same bucket, lookup degrades toward O(n), but Python's hash function and automatic table resizing make this vanishingly rare for integer keys in practice. Compare this to storing the same data as a Python list of (match_id, data) tuples and searching it with a loop: every lookup, even for the first match ever inserted, requires scanning up to n tuples in the worst case, and n/2 on average. With 2 matches the difference is imperceptible. With 5,000 concurrently tracked matches — unrealistic for a single IPL slate, but the kind of scale a national score-aggregation service might carry across multiple leagues — and thousands of polling requests per second, the O(n) list scan would multiply per-request latency by a factor that grows with the size of the dataset, while the O(1) dict lookup stays flat. This is precisely why the API layer's choice of underlying data structure is a real system-design decision, not an implementation detail to skip past.
It is also, however, the aspect of this code least suited to production. The Python process holding matches loses everything the moment it restarts, crashes, or gets redeployed — normal, frequent events in any real deployment. A production score service replaces the dictionary with a database table, something like matches(match_id INTEGER PRIMARY KEY, team_a TEXT, team_b TEXT, overs REAL, score_a INTEGER, wickets_a INTEGER), queried with SELECT and updated with UPDATE statements instead of dict indexing. The view functions barely change — get_score still takes a match_id and returns a dict-shaped JSON response — but the storage underneath becomes durable. The REST interface and the storage engine are deliberately separable; that separability is itself part of what "uniform interface" buys you.
| Status code | Used when | In this API |
|---|---|---|
| 200 OK | Request succeeded, response body present | Successful GET or POST |
| 400 Bad Request | Client sent malformed or incomplete input | POST body missing "runs" |
| 404 Not Found | No resource at this URI | Unknown match_id, or unmatched route |
| 409 Conflict | Request is valid but contradicts current state | Would apply to an all-out innings, see below |
The Misconception: "Stateless" Does Not Mean "No Storage Allowed"
Students who first meet the word "stateless" often conclude that a stateless API cannot keep any data at all — that the matches dictionary above somehow violates REST because it persists information across requests. This gets the constraint backwards. Statelessness governs the relationship between a specific client and the server across requests, not whether the server is allowed to store data at all. The rule is: the server must not keep per-client session context that a later request implicitly depends on. It says nothing about server-side resource data. matches is exactly that — resource data, shared across every client, looked up fresh from each independent request's own path and body. What would break statelessness is something like the server remembering "the last client that hit this endpoint asked about match 101, so assume this next request is also about match 101" — inferring context from history instead of requiring the client to state it every time. The database (or dictionary) holding your application's actual data is not just permitted under REST; it is expected. Nearly every real REST API — banking, e-commerce, IRCTC seat availability — is backed by a database and is still fully stateless, because "stateless" describes the request/response contract, not the presence of a data store behind it.
Active Recall
Attempt each question before reading its answer.
1. In plain terms, does the statelessness constraint forbid the matches dictionary from holding data between requests?
2. A client sends GET /matches/205/score, and 205 is not a key in matches. Trace what the router, the view function, and the final response contain, including the status code.
3. Suppose match 101 already has wickets_a equal to 10 (all out), and a client sends POST /matches/101/score with body {"runs": 4}. Trace the full effect through the code shown above. Does the API stop this update? What is score_a afterward, and what does this reveal?
4. Why does a request to /matches/abc/score get rejected with a 404 instead of crashing inside get_score with a type error?
5. If matches held 5,000 entries instead of 2, compare the time complexity of matches.get(match_id) against scanning a list of (match_id, data) tuples for the same lookup. Which does the code use, and why does the choice matter under heavy polling traffic?
6. Name one concrete change required before shipping this API to real users, and explain why in-memory storage is unsafe for that purpose.
Answers
1. No. Statelessness constrains what the server remembers about a specific client between that client's requests — it does not forbid server-side resource storage shared across all clients. matches is resource data, not session state: every request still supplies everything it needs (the match_id in the URL, the runs in the body) rather than relying on the server recalling a prior interaction.
2. The router matches the URL pattern (205 converts cleanly to an int) and dispatches to get_score(match_id=205). Inside the function, matches.get(205) returns None since no such key exists. The if match is None branch fires, returning jsonify({"error": "match not found"}), 404. The client receives HTTP 404 with body {"error": "match not found"}.
3. Nothing in update_score checks wickets_a before applying the update. match_id in matches is true, the payload has a "runs" key, so matches[101]["score_a"] += 4 executes unconditionally — the score increases even though the team is already all out. The API does not stop this because REST enforces exactly the validation logic you write into the view function and nothing more; the framework and the URL structure provide no business-rule protection by themselves. The gap is fixed by adding an explicit guard, e.g. if matches[match_id]["wickets_a"] >= 10: return jsonify({"error": "innings over"}), 409, using 409 Conflict because the request is well-formed but contradicts the resource's current state.
4. The int converter is enforced at the routing layer, before any view function runs. Werkzeug only considers the rule a match if the URL segment converts successfully to an integer; "abc" fails that conversion, so the rule simply does not match. Since no other registered rule matches /matches/abc/score either, Werkzeug falls through to its built-in 404 handler — your Python code is never invoked, so it cannot crash on a bad type.
5. matches.get(match_id) is a hash table lookup: average O(1), because the key is hashed directly to a bucket regardless of how many entries the table holds. Scanning a list of tuples for a matching id is O(n): every lookup potentially checks every entry. The code uses the dict, which is why it scales flat under load — at IPL-peak polling rates, thousands of requests per second hitting an O(1) store keep response latency roughly constant as the number of live matches grows, whereas an O(n) list scan would make every single lookup slower as more matches were added, directly degrading throughput and response time under exactly the traffic pattern the API is built for.
6. Replace the in-memory matches dictionary with a persistent database table. The Python process's memory — including matches — is entirely wiped on every restart, crash, or redeploy, which are routine events for any server running continuously; a database writes to disk independently of the process's lifetime, so match data survives exactly those events that in-memory storage cannot.
Think About It
Think about this: How would you explain building a rest api with flask to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind building a rest api with flask, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.