Building a REST API with Flask
Open the IRCTC app and check your PNR status. The app does not already know whether your train is running late — your phone has no idea about the Indian Railways database sitting in a server room somewhere. What actually happens is this: your phone sends a short message asking "what is the status of PNR 4521098765?" to a server, the server looks it up, and sends back an answer. Open Cricbuzz during a match and the score updates every few seconds without you reloading the whole app — again, your phone is quietly asking a server "what is the score now?" and getting a fresh answer each time.
This chapter is about exactly that conversation — how one program asks another program a question over the internet, and how to build the program that answers. The technique is called a REST API, and the tool we will use to build one is a small Python library called Flask. By the end, you will have written and traced, line by line, a working API that a phone app or a browser could actually talk to.
The waiter, not the kitchen
Before any code, picture a restaurant. As a customer, you never walk into the kitchen and cook your own food. You look at a fixed menu, tell the waiter what you want, and the waiter goes to the kitchen, gets it prepared, and brings it back to your table on a plate, arranged the same way every time. You didn't need to know how the kitchen is organised, which gas burner was used, or who the chef was. You only needed to know the menu and how to place an order.
An API (Application Programming Interface) works the same way for software. A weather app on your phone does not have a satellite bolted to the back of it. Instead, it sends a request to a weather server's API — the "waiter" — asking for today's forecast for Bengaluru. The server (the "kitchen") does the actual work of fetching and calculating the forecast, and hands back the result in a fixed, predictable format. The app never needs to know how the server stores its data or which programming language it uses internally.
Now stretch the analogy one notch further, because this is exactly where REST comes from. A good restaurant menu is organised by dish — "Masala Dosa," "Butter Chicken" — not by kitchen equipment. In REST, we organise an API by resource — a "thing" the API knows about, like a student, a train, or a cricket match — and we act on that resource using a small, fixed set of standard actions: fetch it, create it, update it, or delete it. That is the whole idea of REST in one sentence: identify things with addresses (URLs), and act on them with a small standard vocabulary of actions. We will now unpack both halves of that sentence formally.
What actually travels over the wire
When your phone talks to a server, it isn't sending vague English sentences — it's sending a structured message called an HTTP request, and getting back a structured message called an HTTP response. Every request has three parts worth knowing at this stage:
- Method — what kind of action is being requested. The two you will use constantly are
GET(fetch/read data, nothing changes on the server) andPOST(send new data, something gets created or changed). - URL (path) — the address of the specific resource, e.g.
/students/2means "student number 2," similar to how/pnr/4521098765would mean "this specific PNR." - Body (only for methods like POST) — the actual data being sent, usually written in a format called JSON (JavaScript Object Notation), which looks almost exactly like a Python dictionary:
{"name": "Aditi", "marks": 88}.
Every response, in turn, carries back two important parts:
- Status code — a three-digit number telling you what happened:
200means "OK, here is your data,"201means "OK, something new was created,"404means "that resource does not exist," and400means "your request was malformed." - Body — the actual data being returned, again usually in JSON.
Mapping this back to the restaurant: the method is what you're asking the waiter to do (order a new dish vs. asking what's already on your table), the URL is which dish on the menu you mean, the request body is special instructions ("less spicy"), and the status code is the waiter saying "coming right up" or "sorry, we're out of that today."
Correcting a common misconception: REST does not mean "uses JSON"
A very common mix-up is to think REST is JSON, or that any API returning JSON is automatically "RESTful." That's incorrect. REST (Representational State Transfer) is a set of design rules for organising an API — mainly, that resources are identified by URLs and manipulated using the standard HTTP methods, and that each request is stateless (the server does not need to remember anything about your previous requests; every request must carry all the information needed to handle it, such as your login token, on its own). JSON is simply the most common data format people choose to send back, because it maps so cleanly onto Python dictionaries and lists — but you could technically build a REST API that returns plain text or XML instead. What makes an API "RESTful" is the URL + method design, not the data format.
Meet Flask
Flask is a small Python library ("microframework") that does one job extremely well: it lets you write a normal Python function, and then tell Flask "run this function whenever a request arrives for this URL." You install it once with pip install flask, and then a complete, runnable API can be as short as this:
from flask import Flask
app = Flask(__name__)
@app.route("/hello")
def hello():
return "Hello, Meera!"
if __name__ == "__main__":
app.run(debug=True)
Let's trace exactly what this does, line by line, because every Flask app you ever write will follow this same skeleton:
app = Flask(__name__)creates the Flask application object — think of it as switching on the restaurant and getting ready to take orders.@app.route("/hello")is a decorator: it tells Flask "add an entry to your menu — whenever a request arrives for the URL path/hello, call the function written directly below me."def hello(): return "Hello, Meera!"is the function that actually runs. Flask takes whatever it returns and sends it back as the response body.app.run(debug=True)starts a small web server on your own computer, listening for incoming requests, typically printing something likeRunning on http://127.0.0.1:5000.
If you now open a browser and visit http://127.0.0.1:5000/hello, this exact sequence happens: your browser sends a GET request for the path /hello → Flask checks its routing table → finds a match → calls hello() → the function returns the string "Hello, Meera!" → Flask wraps that in an HTTP response with status code 200 (the default when nothing else is specified) → your browser displays it.
Correcting a second misconception: debug=True does not put your API "on the internet"
A very natural mistake, seeing that printed address, is to think your API is now live and reachable by anyone in the world, the way a real website is. It is not. The number 127.0.0.1 is a special address that always means "this same computer" — so only programs running on your own laptop can reach it. Making an API reachable from other devices (your phone, a friend's computer, the wider internet) requires additional steps — configuring the host address and, usually, deploying the app onto a proper server — that are beyond a first Flask app. Until then, treat app.run(debug=True) as a private rehearsal room, not a stage.
A route that computes something: numbers first
Real APIs almost always need information from the URL itself — which student, which train, which number. Flask lets you capture parts of the URL as variables using angle brackets. Let's build the simplest possible example: an API that adds two numbers.
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/add/<int:a>/<int:b>")
def add_numbers(a, b):
result = a + b
return jsonify({"a": a, "b": b, "sum": result})
if __name__ == "__main__":
app.run(debug=True)
Trace this carefully for the URL /add/23/19:
- Flask matches the incoming path
/add/23/19against the pattern/add/<int:a>/<int:b>. The<int:a>part means "capture this URL segment, convert it from text to an integer, and pass it into the function as the argument nameda." Here,abecomes the integer23andbbecomes the integer19. - Inside
add_numbers,result = a + bcomputes23 + 19 = 42. jsonify({...})is a Flask helper that converts a Python dictionary into a proper JSON HTTP response (it also sets the response's content type correctly, which plainreturnof a dictionary-like string would not do reliably).- The response body sent back is exactly:
{"a": 23, "b": 19, "sum": 42}, with status code200.
Notice the type conversion matters. If the route pattern had used <a> instead of <int:a>, Flask would capture a as the text "23", and a + b would perform text concatenation, producing "2319" instead of the sum 42 — a classic bug. Always match the converter to the kind of data you expect.
A real resource: an API for student marks
A calculator is a good first route, but REST is really about resources — things with an identity that persist between requests, like students in a class register. Let's build one, backed for now by a plain Python dictionary standing in for a database:
from flask import Flask, request, jsonify
app = Flask(__name__)
students = {
1: {"name": "Aditi", "marks": 88},
2: {"name": "Rohan", "marks": 76}
}
@app.route("/students", methods=["GET"])
def get_all_students():
return jsonify(students)
@app.route("/students/<int:roll_no>", methods=["GET"])
def get_student(roll_no):
student = students.get(roll_no)
if student is None:
return jsonify({"error": "Student not found"}), 404
return jsonify(student)
@app.route("/students", methods=["POST"])
def add_student():
new_data = request.get_json()
new_roll_no = max(students.keys()) + 1
students[new_roll_no] = {"name": new_data["name"], "marks": new_data["marks"]}
return jsonify(students[new_roll_no]), 201
if __name__ == "__main__":
app.run(debug=True)
Notice that the same URL, /students, appears twice — once for GET, once for POST. This is the heart of REST design: the URL identifies the resource ("the collection of students"), and the method identifies the action ("read the collection" vs. "add to the collection"). Flask tells these apart using the methods=[...] argument; a plain @app.route without it defaults to GET only.
Let's trace three separate requests against this exact code, one at a time.
Request 1 — GET /students/2. Flask matches this against the /students/<int:roll_no> pattern, so roll_no = 2. Inside get_student, students.get(2) looks up key 2 in the dictionary and finds {"name": "Rohan", "marks": 76}. Since this is not None, the if block is skipped, and the function returns jsonify({"name": "Rohan", "marks": 76}). No explicit status code is given, so Flask defaults to 200. Final response: status 200, body {"name": "Rohan", "marks": 76}.
Request 2 — GET /students/5. Here roll_no = 5. students.get(5) finds nothing in the dictionary (only keys 1 and 2 exist so far) and returns None. The if student is None condition is now true, so the function returns jsonify({"error": "Student not found"}) paired with , 404 — this comma-separated second value is how Flask lets a view function override the default status code. Final response: status 404, body {"error": "Student not found"}. This is exactly why a 404 exists as a status code: it distinguishes "the server worked fine, but this specific thing doesn't exist" from a server crash.
Request 3 — POST /students with JSON body {"name": "Kiran", "marks": 91}. Inside add_student, request.get_json() reads the request body and parses it into the Python dictionary {"name": "Kiran", "marks": 91}, stored in new_data. Next, max(students.keys()) looks at the existing keys — at this point still {1, 2} — and returns 2, so new_roll_no = 2 + 1 = 3. The line students[3] = {"name": "Kiran", "marks": 91} adds a brand-new entry to the dictionary. Finally, the function returns jsonify(students[3]), i.e. {"name": "Kiran", "marks": 91}, together with status code 201 — the correct code for "something new was successfully created," as opposed to the generic 200. If you now sent a new GET request for roll number 3, it would successfully find Kiran, because the dictionary was permanently changed by the POST — as long as the Flask server process keeps running. (Restart the server and the dictionary resets to just Aditi and Rohan, since nothing was saved to a real file or database — a limitation of using a plain dictionary that a genuine app would fix with a database.)
Correcting a third misconception: GET and POST are not interchangeable conveniences
Some beginners, once they see that both methods can technically carry data, treat GET and POST as interchangeable — for instance, trying to "log in" by putting a username and password as part of a GET URL, like /login?user=amit&pass=1234. This is a real and dangerous mistake, not just a style preference. Data sent via GET becomes part of the URL, and URLs are routinely stored in browser history, server access logs, and shared screenshots — exactly where you do not want a password sitting in plain text. POST request bodies are not shown in the address bar and are not logged the same way by default. The rule of thumb: GET is for fetching data that causes no side effect and is safe to bookmark or repeat; POST (and its cousins PUT and DELETE, which you will meet as you go further) is for anything that changes data or carries sensitive information.
Seeing the whole request-response cycle
Here is the entire journey from Request 1 above, drawn out as a diagram — a client asks a question, Flask's routing table figures out which function should answer it, that function runs and produces an answer, and the answer travels back.
Where this fits in your CBSE studies
Your Class 8 Computer Science / AI curriculum has already given you the two building blocks this chapter depends on: Python functions and Python dictionaries. A Flask route is simply a function you have already known how to write, connected to a URL by one new line — the @app.route(...) decorator. What is genuinely new here is the idea of a program that waits for requests instead of running once and stopping, and the vocabulary around it — HTTP methods, status codes, JSON — which is exactly the same vocabulary used to describe how every mobile app, banking portal, and government e-service (such as DigiLocker or the UPI apps you may have seen adults use) actually fetches and updates data behind the scenes. Higher classes and Informatics Practices go on to connect Flask routes to real databases instead of a plain dictionary, but the request → route match → function → response skeleton you traced above stays identical.
Check your understanding
Work these out by tracing the code above line by line, the same way we did — do not guess.
- Using the
add_numbersroute, what exact JSON body and status code wouldGET /add/7/15produce? - Suppose the
studentsdictionary currently has keys 1, 2, and 3 (Aditi, Rohan, Kiran). A new request arrives:POST /studentswith body{"name": "Devika", "marks": 95}. What roll number is assigned, what is the exact response body, and what status code comes back? - A classmate writes a route as
@app.route("/add/<a>/<b>")(noint:converter) and then computesa + b. For the request/add/7/15, what does this route actually return, and why is it wrong? - Why is
{"error": "Student not found"}, 404a better response than simply returning{"error": "Student not found"}with the default status code? - Explain, using the restaurant analogy, why the same URL
/studentscan correctly do two completely different things depending on whether the request method is GET or POST. - Why is it unsafe to send a login password as part of a GET request's URL, even if the server would technically accept it?
- You show your Flask app to a friend and it prints
Running on http://127.0.0.1:5000. Your friend, on their own phone, on a different WiFi network, tries to open that same address and it fails. Explain why.
Summary
A REST API lets one program ask another program for data or ask it to make a change, over the network, using a small standard vocabulary. Every exchange is an HTTP request (a method like GET or POST, a URL identifying a resource, and sometimes a JSON body) answered by an HTTP response (a status code like 200, 201, or 404, and usually a JSON body). REST specifically means organising these requests around resources identified by URLs and acted on through standard methods, with each request handled independently — it does not mean "returns JSON," and JSON is only the popular convention, not the definition. Flask turns this design into a few lines of Python: Flask(__name__) creates the app, @app.route(...) registers which function answers which URL and method, converters like <int:roll_no> pull typed values straight out of the URL, request.get_json() reads an incoming JSON body, and jsonify(...), optionally paired with an explicit status code, sends the response back. Tracing every example above by hand — matching the URL to the route, running the function's actual Python statements, and reading the final status and body — is the same skill you will use to debug any Flask app you write from here on.
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.