When you use a web application, a conversation is happening behind the scenes. Your browser asks a question. A Python program on a server receives that question, talks to a database, and sends an answer back. Your browser displays the answer. Every web application you have ever used — Instagram, YouTube, your school's grading system — is built on this same back-and-forth. This article shows you how that conversation works in the stack you are using in this course.
This article is organised in layers. Each section starts with the basic idea — what every Grade 9 programmer needs to understand. Then it goes deeper, and deeper still.
| Layer | What it means | When to read it |
|---|---|---|
| Basic idea | What every programmer needs. Read this first, always. | Right now. |
| At a deeper level | The mechanism behind the basic idea. Helps you reason about where things go wrong. | Once you have run the starter app and seen a request complete. |
| At the deepest level | How the tools work under the hood. Not required for Grade 9 — but explains things that will otherwise seem mysterious. | When you are curious, or when something breaks and you cannot figure out which layer is responsible. |
1 The Big Picture — Five Layers, One Request
Every time a user does something in your web app — loads a page, clicks a button, submits a form — five things happen in sequence:
- The browser sends a request. It asks for data or an action.
- Flask receives the request. Your Python code runs.
- The database is queried. Data is read or written.
- Flask builds a response. Usually a chunk of JSON data.
- The browser receives the response and updates what the user sees.
These five steps happen every time. Understanding which step is responsible for what makes bugs far easier to find.
Here is the same sequence with the specific tools your course uses mapped onto each step:
| Step | Tool | What actually happens |
|---|---|---|
| 1. Request | Browser + JavaScript (fetch) | JavaScript in the HTML page calls fetch('/api/students'). The browser sends an HTTP request to the Flask server. |
| 2. Routing | Flask | Flask matches the URL (/api/students) to a Python function decorated with @app.route. That function runs. |
| 3. Database query | Flask-SQLAlchemy → MySQL | The Python function calls Flask-SQLAlchemy, which translates the call into a SQL query and sends it to the MySQL server on the school network. |
| 4. Response | Flask (jsonify) | MySQL returns rows of data. Flask packages them as JSON and sends the HTTP response back to the browser. |
| 5. Display | JavaScript + Tabler (HTML) | JavaScript receives the JSON, reads the data, and updates the page — adding rows to a table, populating a card, or showing a message. |
Almost every bug in a web app lives in exactly one of these five layers. The skill is not fixing bugs — it is diagnosing which layer the bug is in before touching any code. A blank page might mean the request never arrived (Step 1), the route didn't match (Step 2), the query returned nothing (Step 3), the JSON was malformed (Step 4), or the JavaScript failed to insert the data (Step 5). Different layer, different fix.
The conversation between browser and Flask uses HTTP — HyperText Transfer Protocol. Every request is a text message sent over the network following a strict format: a method (GET, POST, PUT, DELETE), a URL, headers (metadata about the request), and optionally a body (the data being sent).
Flask is a WSGI application — Web Server Gateway Interface. WSGI is a Python standard that defines how a web server hands a request to a Python application and receives a response back. When you run flask run in development, Flask starts a built-in WSGI server. In production, a dedicated server like Gunicorn or uWSGI takes that role. The Flask application itself does not change — only what sits in front of it.
The conversation between Flask-SQLAlchemy and MySQL uses a separate protocol — the MySQL wire protocol — over a TCP connection to the school MySQL server. Flask-SQLAlchemy (via PyMySQL) opens this connection when your app starts, keeps it in a connection pool, and reuses it for subsequent queries rather than reconnecting every time. This is why your SQLALCHEMY_DATABASE_URI config line contains a hostname, port, username, password, and database name — every field is needed to establish that TCP connection.
2 The Browser — Where the Request Begins
The browser is what the user sees and interacts with. It displays HTML and runs JavaScript. When a user clicks a button or the page loads, JavaScript code fires and sends a request to Flask asking for data. The browser does not talk to the database directly — it only ever talks to Flask.
In this course, JavaScript uses the fetch function to send requests. Here is a minimal example:
fetch('/api/students')
.then(response => response.json())
.then(data => {
console.log(data);
// update the page with data here
});Breaking this down:
fetch('/api/students')sends aGETrequest to the URL/api/studentson the same server. Flask must have a route that matches this URL exactly..then(response => response.json())takes the raw HTTP response and parses its body as JSON — turning the text into a JavaScript object..then(data => { ... })receives that JavaScript object and lets you do something with it: add rows to a table, show a message, fill in a form.
When something goes wrong, the browser's developer tools are your first debugging stop. Open them with F12 (or Cmd+Option+I on Mac). The Network tab shows every request your page made and whether Flask responded — and with what. The Console tab shows JavaScript errors. If the Network tab shows a 404 or 500 status code, the problem is in Flask, not JavaScript.
fetch is asynchronous — it does not stop JavaScript from running while it waits for the server to respond. Instead, it returns a Promise: a placeholder for a value that will arrive later. The .then() calls register callback functions that run when the promise resolves. This is why you cannot do const data = fetch(...) and then immediately use data — the data hasn't arrived yet. This asynchronous model is fundamentally different from Python's top-to-bottom execution, and it is the source of most JavaScript confusion for Python programmers.
Modern JavaScript allows async/await syntax as an alternative to .then() chains — it makes asynchronous code look synchronous without blocking the browser. You may see both patterns in production code.
3 Flask — The Gatekeeper
Flask is a Python program running on a server. Its job is to listen for incoming requests, figure out what the request is asking for, run the appropriate Python code, and send a response back. You define the available URLs and what happens at each one by writing Python functions with a special decorator.
@app.route('/api/students')
def get_students():
# do something
return jsonify(result)When a request arrives at /api/students, Flask calls get_students(). Whatever that function returns becomes the response sent to the browser.
Flask does three things when a request arrives:
- Routing. Flask compares the incoming URL against all registered routes. If it finds a match, it calls the associated function. If it does not, it returns a 404 response automatically.
- Running your function. Your Python code executes. This is where you query the database, process data, and decide what to send back.
- Building the response.
jsonify()takes a Python dictionary or list and converts it to a JSON string, then wraps it in an HTTP response with the correct headers so the browser knows it is receiving JSON.
| HTTP status code | What it means | What to check |
|---|---|---|
| 200 OK | Everything worked. The response body contains the data. | Check the response body if the data is wrong. |
| 404 Not Found | No route matched the URL. | Check for a typo in the URL in JavaScript or in the @app.route decorator. |
| 405 Method Not Allowed | The route exists but does not accept this HTTP method (e.g. a POST to a GET-only route). | Check the methods= parameter in @app.route. |
| 500 Internal Server Error | Your Python code crashed while handling the request. | Read the Flask terminal output. There will be a full traceback. |
When the browser shows a 500 error, Flask's terminal output (where you ran flask run) will contain a full Python traceback — the same kind you have already learned to read. The browser cannot tell you what the Python error is. The terminal can. Always have the terminal visible when developing.
The @app.route decorator is syntactic sugar for calling app.add_url_rule(). Flask stores all registered routes in a URL map — a data structure that maps URL patterns to view functions. When a request arrives, Flask uses Werkzeug (the library Flask is built on) to match the incoming URL against that map. Werkzeug supports dynamic segments like /api/students/<int:id>, which capture part of the URL as a function argument.
Each request in Flask runs inside a request context — a temporary object pushed onto a stack at the start of the request and popped at the end. The request object you import from Flask (from flask import request) is a proxy to this context. This is why you can access request.args and request.json inside a route function without passing anything in — Flask makes those objects available automatically during the request lifecycle. It is also why accessing request outside a route function raises a RuntimeError: working outside of request context.
4 The Database — Where Data Lives
The database stores data permanently. Unlike a Python variable, which disappears when the program stops, the database remembers everything even after the server restarts. In this course you use MySQL, a relational database. Data is stored in tables — like spreadsheets — with rows and columns. Flask talks to MySQL using Flask-SQLAlchemy, which lets you interact with the database using Python instead of writing raw SQL.
Flask-SQLAlchemy works in two steps:
- You define a model. A model is a Python class where each attribute represents a column in the database table.
- You query the model. SQLAlchemy translates Python method calls into SQL queries, sends them to MySQL, and returns the results as Python objects.
# Step 1: define the model
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
grade = db.Column(db.Integer)
# Step 2: query it inside a route
@app.route('/api/students')
def get_students():
students = db.session.execute(select(Student)).scalars().all()
result = [{"id": s.id, "name": s.name, "grade": s.grade}
for s in students]
return jsonify(result)The SQLAlchemy call db.session.execute(select(Student)) is translated behind the scenes into:
SELECT id, name, grade FROM student;
MySQL runs that query, returns rows, and SQLAlchemy converts each row into a Student object. Your Python code then converts those objects into plain dictionaries, which jsonify can convert to JSON.
If you only ever use SQLAlchemy, you know the tool but not what it is doing. When something returns the wrong data, or nothing at all, you cannot debug it without knowing what SQL was generated. In this course, you learn raw SQL first — that is Unit 3, Weeks 17–18. Once you can write the SQL by hand, the SQLAlchemy equivalent is obvious. Skipping SQL and going straight to SQLAlchemy is like learning to drive using only automatic transmission and never understanding what gears do.
SQLAlchemy has two layers. The Core layer is a SQL expression language — you build SQL programmatically using Python objects, and it generates the query string. The ORM layer (which this course uses) sits on top of Core and adds the model-class abstraction: each class maps to a table, each instance maps to a row.
Queries in the ORM are lazy by default — select(Student) does not hit the database immediately. The query is only executed when you call db.session.execute() and then materialise the results with .scalars().all(). This deferred execution model is what allows SQLAlchemy to optimise and compose queries before sending them.
The session (db.session) is SQLAlchemy's unit-of-work manager. It tracks all objects you have loaded or modified in the current request. When you call db.session.commit(), it writes all pending changes to the database in a single transaction. If anything fails, you call db.session.rollback() to undo all changes atomically. In Flask-SQLAlchemy, the session is automatically scoped to the current request — it is created when a request begins and closed (with a rollback of any uncommitted changes) when the request ends.
5 The Response — Data Coming Back
After Flask queries the database, it packages the data as JSON and sends it back to the browser. JSON is a text format for representing data — it looks a lot like Python dictionaries and lists. The browser's JavaScript receives the JSON, reads the data out of it, and updates the page.
// What Flask sends:
{"students": [{"id": 1, "name": "Alex", "grade": 91},
{"id": 2, "name": "Sam", "grade": 78}]}
// What JavaScript does with it:
data.students.forEach(student => {
console.log(student.name, student.grade);
});The full round trip, with data shown at each stage:
| Stage | What the data looks like | Format |
|---|---|---|
| MySQL returns rows | Raw database rows: tuples of values | MySQL wire protocol (binary) |
| SQLAlchemy converts rows | Python objects: Student(id=1, name="Alex", grade=91) | Python objects in memory |
| Your route converts objects | Python dict: {"id": 1, "name": "Alex", "grade": 91} | Python dict in memory |
jsonify serialises | JSON string: {"id": 1, "name": "Alex", "grade": 91} | Text in HTTP response body |
| JavaScript parses | JavaScript object: { id: 1, name: "Alex", grade: 91 } | JavaScript object in browser memory |
| JavaScript updates the DOM | HTML element added to the page: a table row, a card, a list item | HTML in the browser |
JSON uses double quotes only (never single). It has no True/False — it uses true/false (lowercase). It has no None — it uses null. Python's jsonify() and json.dumps() handle these conversions for you. If you ever manually build a JSON string in Python and something looks wrong on the JavaScript side, this is usually why.
jsonify() calls Python's json.dumps() under the hood, with Flask-specific handling for a few types that the standard library cannot serialise by default (like datetime objects). The result is wrapped in a Response object with Content-Type: application/json. This header is important: it tells the browser's fetch API what kind of data to expect, which is why response.json() in JavaScript works without you specifying a format.
The HTTP response itself has three parts: a status line (HTTP/1.1 200 OK), headers (key-value metadata including Content-Type and Content-Length), and a body (the actual JSON string). All of this travels over a TCP connection that the browser opened to the Flask server. HTTP/1.1 can reuse that connection for multiple requests (keep-alive). HTTP/2, used by production servers, multiplexes many requests over a single connection simultaneously. In development with flask run, you are using HTTP/1.1.
6 When Things Go Wrong — Which Layer Is It?
Every bug in a web app lives in one layer. The fastest way to fix a bug is to identify the layer first, then look at the code for that layer.
| Symptom | Most likely layer | First thing to check |
|---|---|---|
| Nothing happens when I click the button | Browser / JavaScript | Browser console (F12 → Console). Is there a JavaScript error? |
| 404 in the Network tab | Flask routing | Does the URL in fetch() exactly match the URL in @app.route? |
| 500 in the Network tab | Flask / Python crash | Read the Flask terminal output. There is a traceback there. |
| Request succeeds but data is wrong or empty | Database query | Run the equivalent SQL query directly in MySQL. Is the data there? |
| Data arrives but the page does not update | JavaScript / DOM | console.log(data) immediately after .then(data => ...). Is the data actually there? |
The diagnostic workflow for a broken web request:
- Open the browser developer tools (F12). Go to the Network tab. Reproduce the problem. Find the request in the list.
- Check the status code. 200 means Flask responded. 404 means the route did not match. 500 means Flask crashed.
- If 200 but wrong data: click the request in the Network tab and read the Response body. Is the JSON what you expected? If not, the bug is in Flask or the database.
- If 500: switch to the Flask terminal. Read the traceback. Identify the line number. Fix the Python error.
- If the page still does not update despite correct JSON: add
console.log(data)inside the.then()block. If the data is there, the bug is in the DOM-manipulation code. If it is not, the JSON is being misread.
console.log() is the JavaScript equivalent of print(). During debugging, log the data at every stage: after fetch returns, after parsing, after each transformation. Remove them once the bug is fixed. Leaving console.log calls in production code is considered poor practice — exactly like leaving stray print() statements in Python.
For complex bugs that span multiple layers, professionals use tools that give visibility into each layer independently: Flask's debug toolbar shows timing, SQL queries executed, and request/response details inline in the browser. SQLAlchemy's <strong>echo=True</strong> option prints every SQL query to the terminal as it is sent — essential for verifying that the ORM is generating the query you think it is. Wireshark can inspect raw HTTP and MySQL packets on the network, though this is rarely needed at Grade 9.
Understanding the stack also means understanding where security boundaries are. The browser is untrusted — any user can open DevTools and modify requests. Flask must validate all incoming data before trusting it. The database must never be exposed directly to the browser — only Flask, running on the server, should have database credentials. This is why the architecture is layered: each layer is a checkpoint that validates and transforms data before passing it on.
7 The Complete Picture
| Layer | Tool in this course | Responsibility | When it goes wrong |
|---|---|---|---|
| Interface | HTML + Tabler | Displays data to the user. Provides buttons, forms, and layout. | Data arrives but the page looks wrong. CSS or HTML structure problem. |
| Interactivity | Vanilla JavaScript (fetch) | Sends requests to Flask. Receives JSON. Updates the page. | Nothing happens on click. Console error. Page does not update despite correct data. |
| Web server | Flask | Routes requests to Python functions. Runs business logic. Returns JSON. | 404 (route not found) or 500 (Python crash). Read the terminal. |
| Database access | Flask-SQLAlchemy | Translates Python model calls into SQL. Returns Python objects. | Empty results. Wrong data. SQL error in the terminal. |
| Database | MySQL (school server) | Stores data permanently. Answers SQL queries. | Connection refused. Query returns unexpected rows. Data missing. |
A web application is not one program — it is five layers passing data to each other. The browser asks, Flask routes, SQLAlchemy queries, MySQL answers, Flask responds, JavaScript displays. Every bug lives in exactly one of those layers. The fastest path to a fix is always the same: identify the layer, look at the evidence that layer produces (the browser console, the Flask terminal, the SQL output), and diagnose before you touch any code.
- Describe the five steps that happen when a user clicks a button in a web app built with this stack. Do not look at the article — write them from memory.
- A student loads their app and the page is blank. The browser Network tab shows a 404 for the request to
/api/scores. What are the two most likely causes, and how would you check for each? - Explain in one sentence what
jsonify()does and why it is needed. - A student's route function runs without crashing and returns data, but the data on the page is stale — it shows yesterday's values, not today's. Which layer is most likely responsible? What is the first thing you would check?
- Why does the browser never talk directly to the MySQL database? Give two reasons.
- A student gets a 500 error in the browser. Where should they look for the error message, and what kind of information will they find there?