Generating PDF…

Preparing…
← Back

Flask for Beginners

Build Your First Web API with Python

Computer Programming 2 · Week 8 Session 2 (Enhanced)

What You Will Learn Today

Concepts

  • What a web server / API is
  • What Flask is and why use it
  • HTTP methods: GET, POST, PUT, DELETE
  • HTTP status codes (200, 201, 400, 404…)
  • Routes, path params, query params
  • JSON requests and responses

Practice Goals

  • Set up a Flask project from scratch
  • Write Hello World and return JSON
  • Build multiple routes with parameters
  • Accept POST data and echo it back
  • Build a full in-memory Items API
  • Complete two mini-challenges

What is a Web Server?

Think of a web server like a waiter in a restaurant:

  • The customer (browser / app) makes a request
  • The waiter (server) receives it and decides what to do
  • The kitchen (your Python code) prepares the response
  • The waiter brings back the result — usually JSON
Key idea Client sends a request to a URL → Server runs Python code → Server sends back data

Flask is the waiter. You write the kitchen logic.

Client (browser / curl)
↕ HTTP request/response
Flask (your app.py)
↕ function call
Your Python logic
↕ returns dict / JSON
Response sent back

What is Flask?

  • Flask is a micro web framework for Python
  • "Micro" = minimal, you add only what you need
  • Used to build web apps, REST APIs, and backend services
  • Very popular for beginners and production alike
Flask vs Django Flask is small and flexible — great for APIs and learning. Django is larger and has more built-in features (admin, ORM). Start with Flask, grow into Django later.
# The smallest possible Flask app
from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello, world!"

if __name__ == "__main__":
    app.run(debug=True)

# That's it – 8 lines to run a web server!

Setup: Virtual Env + Flask

  1. Create a project folder
  2. Create a virtual environment (isolates packages)
  3. Activate the environment
  4. Install Flask
  5. Create app.py
Why a virtual environment? Each project gets its own packages so they never conflict with each other or the system Python.
# Step 1: Create folder
mkdir my-flask-api
cd my-flask-api

# Step 2: Create virtual environment
python3 -m venv .venv

# Step 3: Activate it
source .venv/bin/activate        # macOS / Linux
# .venv\Scripts\Activate.ps1    # Windows PowerShell

# Prompt changes to: (.venv) $

# Step 4: Install Flask
pip install Flask

# Step 5: Verify
python -c "import flask; print(flask.__version__)"

Your First Flask App

Create app.py in your project folder. Let's break down every line:

  • from flask import Flask — import the Flask class
  • app = Flask(__name__) — create the application object
  • @app.route("/") — register a URL route (the decorator)
  • def home(): — the function that runs when that URL is visited
  • app.run(debug=True) — start the server, auto-restart on changes
Try it Run python app.py then open http://127.0.0.1:5000 in your browser.
from flask import Flask  # ① import

app = Flask(__name__)    # ② create app

@app.route("/")          # ③ register URL
def home():              # ④ view function
    return "Hello, Flask!"

# ⑤ Run the dev server
if __name__ == "__main__":
    app.run(debug=True)

Run the Server and Test It

Open two terminals: one to run the server, one to test it.

Terminal 1 – Start server python app.py
You'll see: Running on http://127.0.0.1:5000
Terminal 2 – Test with curl curl http://127.0.0.1:5000/
Response: Hello, Flask!
Or just use your browser Open http://127.0.0.1:5000/
# Terminal 1 – run the server
python app.py
# * Running on http://127.0.0.1:5000
# * Debug mode: on

# Terminal 2 – test it
curl http://127.0.0.1:5000/
# Hello, Flask!

# curl -v shows headers too:
curl -v http://127.0.0.1:5000/
# HTTP/1.1 200 OK
# Content-Type: text/html
# Hello, Flask!

Routes: Connecting URLs to Functions

Every @app.route("/path") maps a URL to a Python function.

  • You can have as many routes as you need
  • Each function must return something (a string, dict, or tuple)
  • By default, routes only accept GET requests
URL pattern → function name /home()  |  /aboutabout()  |  /pingping()
from flask import Flask
app = Flask(__name__)

@app.route("/")
def home():
    return "Welcome to my API!"

@app.route("/about")
def about():
    return "This is a beginner Flask API."

@app.route("/ping")
def ping():
    return "pong"

@app.route("/version")
def version():
    return "v1.0.0"

if __name__ == "__main__":
    app.run(debug=True)

✏️ Practice: Add Your Own Routes

Task (5 min)

Starting from the Hello World app, add these three routes:

  • /hello — returns "Hello, student!"
  • /today — returns today's date as a string (use datetime)
  • /lucky — returns a random number between 1–100 (use random)
Hint – importing Python modules from datetime import date then str(date.today())
import random then str(random.randint(1, 100))
Expected results curl .../helloHello, student!
curl .../today2026-05-04 (or current date)
curl .../lucky → a random number

Solution: Multiple Routes

Here is one possible solution. Compare with yours:

  • We imported date and random at the top
  • Each route is a simple function that returns a string
  • Calling random.randint(1, 100) every request gives a new value
from flask import Flask
from datetime import date
import random

app = Flask(__name__)

@app.route("/")
def home():
    return "Welcome!"

@app.route("/hello")
def hello():
    return "Hello, student!"

@app.route("/today")
def today():
    return str(date.today())    # e.g. "2026-05-04"

@app.route("/lucky")
def lucky():
    return str(random.randint(1, 100))

if __name__ == "__main__":
    app.run(debug=True)

Returning JSON Responses

APIs return JSON (JavaScript Object Notation), not plain text.

  • Use jsonify() to convert a Python dict to a JSON response
  • It also sets the correct Content-Type: application/json header
  • In Flask 2+, you can also return {"key": "value"} directly
Python dict vs JSON Python dict: {"name": "Ana", "age": 20}
JSON string: {"name": "Ana", "age": 20} (looks the same, but it's text!)
from flask import Flask, jsonify
app = Flask(__name__)

@app.route("/status")
def status():
    # jsonify converts dict → JSON response
    return jsonify({"service": "api", "status": "ok"})

@app.route("/info")
def info():
    data = {
        "name": "My Flask API",
        "version": "1.0",
        "author": "Student"
    }
    return jsonify(data)

# Flask 2+: shortcut (returns dict directly)
@app.route("/ping")
def ping():
    return {"message": "pong"}  # works in Flask 2+

✏️ Practice: JSON Profile Endpoint

Task (5 min)

Create a route /me that returns a JSON object describing yourself (or a fictional student).

  • Include: name, course, year, hobbies (a list)
  • Test: curl http://127.0.0.1:5000/me
Expected response
{
  "name": "Ana",
  "course": "Computer Programming 2",
  "year": 2,
  "hobbies": ["coding", "gaming", "reading"]
}
Bonus Add a /team route that returns a list of such objects (at least 3 members).

Quick Check ✅

Which Flask function sets the Content-Type: application/json header automatically?


Path Parameters

Put <variable> in the route URL to capture parts of the path.

  • /hello/<name> — captures a string
  • /square/<int:n> — captures and converts to int
  • /price/<float:amount> — captures as float
  • The variable is passed as a function argument
URL examples /hello/Ananame = "Ana"
/square/7n = 7 (already an int!)
from flask import Flask, jsonify
app = Flask(__name__)

# String parameter
@app.route("/hello/<name>")
def hello(name):
    return jsonify({"message": f"Hello, {name}!"})

# Integer parameter (auto-converted)
@app.route("/square/<int:n>")
def square(n):
    return jsonify({"n": n, "result": n * n})

# Float parameter
@app.route("/double/<float:x>")
def double(x):
    return jsonify({"input": x, "doubled": x * 2})

✏️ Practice: Path Parameter Routes

Task (8 min)

Add these routes to your app.py:

  • /greet/<name> — returns {"greeting": "Hi, Ana!"}
  • /multiply/<int:a>/<int:b> — returns the product of a × b
  • /upper/<word> — returns the word in UPPERCASE
  • /repeat/<int:times>/<word> — returns the word repeated times times, space-separated
Test commands
curl http://127.0.0.1:5000/greet/Sam
# {"greeting": "Hi, Sam!"}

curl http://127.0.0.1:5000/multiply/6/7
# {"a": 6, "b": 7, "result": 42}

curl http://127.0.0.1:5000/upper/flask
# {"result": "FLASK"}

curl http://127.0.0.1:5000/repeat/3/hello
# {"result": "hello hello hello"}

Solution: Path Parameters

from flask import Flask, jsonify
app = Flask(__name__)

@app.route("/greet/<name>")
def greet(name):
    return jsonify({"greeting": f"Hi, {name}!"})

@app.route("/multiply/<int:a>/<int:b>")
def multiply(a, b):
    return jsonify({"a": a, "b": b, "result": a * b})

@app.route("/upper/<word>")
def upper(word):
    return jsonify({"result": word.upper()})

@app.route("/repeat/<int:times>/<word>")
def repeat(times, word):
    # " ".join([word] * times) repeats the word
    result = " ".join([word] * times)
    return jsonify({"result": result})

if __name__ == "__main__":
    app.run(debug=True)

Query Parameters ?key=value

Query params come after the ? in the URL. They are optional extras.

  • Access them with request.args.get("key")
  • Always provide a default in case they are missing
  • Multiple params: ?name=Ana&age=20
Path param vs Query param Path: /users/42 — the ID is required, part of the resource
Query: /search?q=flask — optional filter or modifier
from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route("/search")
def search():
    # .get() returns None if missing – provide a default!
    q    = request.args.get("q", "")
    page = int(request.args.get("page", 1))
    size = int(request.args.get("size", 10))
    return jsonify({
        "query": q,
        "page": page,
        "page_size": size
    })

# Test: /search?q=flask&page=2&size=5
# {"page": 2, "page_size": 5, "query": "flask"}

✏️ Practice: Query Parameters

Task (8 min)

Create these routes that use query parameters:

  • /greet?name=Ana&greeting=Hello — returns "Hello, Ana!" (use defaults: name=World, greeting=Hi)
  • /add?a=5&b=3 — returns the sum of a and b as JSON
  • /shout?msg=hello&times=3 — returns the message in UPPERCASE repeated times times
Test commands
curl "http://127.0.0.1:5000/greet?name=Sam&greeting=Hey"
# {"message": "Hey, Sam!"}

curl "http://127.0.0.1:5000/greet"
# {"message": "Hi, World!"}   ← defaults

curl "http://127.0.0.1:5000/add?a=10&b=25"
# {"a": 10, "b": 25, "sum": 35}

curl "http://127.0.0.1:5000/shout?msg=hello×=3"
# {"result": "HELLO HELLO HELLO"}

Solution: Query Parameters

from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route("/greet")
def greet():
    name     = request.args.get("name", "World")
    greeting = request.args.get("greeting", "Hi")
    return jsonify({"message": f"{greeting}, {name}!"})

@app.route("/add")
def add():
    a = int(request.args.get("a", 0))
    b = int(request.args.get("b", 0))
    return jsonify({"a": a, "b": b, "sum": a + b})

@app.route("/shout")
def shout():
    msg   = request.args.get("msg", "")
    times = int(request.args.get("times", 1))
    result = " ".join([msg.upper()] * times)
    return jsonify({"result": result})

if __name__ == "__main__":
    app.run(debug=True)

HTTP Methods

The method tells the server what action to take:

MethodActionExample URLBody?
GETRead dataGET /itemsNo
POSTCreate new dataPOST /itemsYes (JSON)
PUTReplace / updatePUT /items/1Yes (JSON)
DELETERemove dataDELETE /items/1No
Flask default Routes only accept GET unless you add methods=["POST"] (or others) to @app.route().

HTTP Status Codes

Every response has a status code that says what happened:

CodeMeaningWhen to use
200OKDefault success for GET/PUT/DELETE
201CreatedPOST that successfully created a resource
400Bad RequestClient sent invalid data (missing field, wrong type…)
404Not FoundThe item the client asked for doesn't exist
405Method Not AllowedWrong HTTP method for this route
500Internal Server ErrorA bug in your Python code
Return a custom status code in Flask return jsonify({"error": "not found"}), 404

Receiving POST Data (JSON Body)

When a client sends data, it goes in the request body (not in the URL).

  1. Add methods=["POST"] to the route
  2. Check request.is_json (optional but good practice)
  3. Parse body with request.get_json()
  4. Validate the fields you need
  5. Return 400 for bad input, 201 for success
Important: curl POST needs two flags -X POST — use POST method
-H "Content-Type: application/json" — tell server the body is JSON
-d '{"key":"value"}' — the actual body
from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route("/echo", methods=["POST"])
def echo():
    # 1. Check Content-Type header
    if not request.is_json:
        return jsonify({"error": "Must send JSON"}), 400

    # 2. Parse the body
    data = request.get_json(silent=True)
    if data is None:
        return jsonify({"error": "Invalid JSON"}), 400

    # 3. Echo it back
    return jsonify({"you_sent": data}), 200

# Test:
# curl -X POST http://127.0.0.1:5000/echo \
#   -H "Content-Type: application/json" \
#   -d '{"name": "Ana", "age": 20}'

✏️ Practice: POST Endpoints

Task (10 min)

Build two POST routes:

  • POST /add-numbers — body: {"a": 5, "b": 3} → returns {"sum": 8}. Return 400 if a or b is missing.
  • POST /register — body: {"username": "ana", "password": "secret"} → returns {"message": "Registered: ana"}. Return 400 if either field is missing or blank.
Test commands
# Add numbers
curl -X POST http://127.0.0.1:5000/add-numbers \
  -H "Content-Type: application/json" \
  -d '{"a": 10, "b": 5}'
# {"sum": 15}

# Missing field (should return 400)
curl -X POST http://127.0.0.1:5000/add-numbers \
  -H "Content-Type: application/json" \
  -d '{"a": 10}'
# {"error": "..."}

Solution: POST Endpoints

from flask import Flask, request, jsonify
app = Flask(__name__)

def bad(msg):
    """Helper: return a 400 error response."""
    return jsonify({"error": msg}), 400

@app.route("/add-numbers", methods=["POST"])
def add_numbers():
    body = request.get_json(silent=True) or {}
    a = body.get("a")
    b = body.get("b")
    if a is None or b is None:
        return bad("Both 'a' and 'b' are required")
    try:
        return jsonify({"sum": float(a) + float(b)})
    except (TypeError, ValueError):
        return bad("'a' and 'b' must be numbers")

@app.route("/register", methods=["POST"])
def register():
    body = request.get_json(silent=True) or {}
    username = (body.get("username") or "").strip()
    password = (body.get("password") or "").strip()
    if not username:
        return bad("username is required")
    if not password:
        return bad("password is required")
    return jsonify({"message": f"Registered: {username}"}), 201

In-Memory Data Store

Before we use a database, we can store data in a plain Python list.

  • Data lives in memory — it resets when the server restarts
  • Great for learning and prototyping
  • We use a global items list and a counter for IDs
Warning In a real app, never store important data in memory only. Use a database (SQLite, PostgreSQL, etc.).
from flask import Flask, request, jsonify
app = Flask(__name__)

# Our "database" in memory
items = []
next_id = 1

@app.route("/items", methods=["GET"])
def list_items():
    return jsonify(items)   # returns the whole list

@app.route("/items", methods=["POST"])
def create_item():
    global next_id
    body = request.get_json(silent=True) or {}
    name = (body.get("name") or "").strip()
    if not name:
        return jsonify({"error": "name is required"}), 400
    item = {"id": next_id, "name": name}
    items.append(item)
    next_id += 1
    return jsonify(item), 201  # 201 Created

Get a Single Item by ID

Let clients request one specific item by its ID.

  • Use a path parameter <int:item_id>
  • Search the list for a matching ID
  • Return 404 if the item doesn't exist
Python tip – next() with default next((i for i in items if i["id"]==item_id), None)
Returns the first match or None if not found.
from flask import Flask, jsonify
app = Flask(__name__)

items = [{"id": 1, "name": "apple"}, {"id": 2, "name": "banana"}]

def find_item(item_id):
    """Return item with matching id, or None."""
    return next((i for i in items if i["id"] == item_id), None)

@app.route("/items/<int:item_id>", methods=["GET"])
def get_item(item_id):
    item = find_item(item_id)
    if item is None:
        return jsonify({"error": "Item not found"}), 404
    return jsonify(item)

# Test:
# curl http://127.0.0.1:5000/items/1  → {"id":1,"name":"apple"}
# curl http://127.0.0.1:5000/items/99 → 404 not found

Full CRUD Walkthrough with curl

Try these commands in order. Each builds on the previous one.

# 1. LIST (empty)
curl http://127.0.0.1:5000/items
# []

# 2. CREATE two items
curl -X POST http://127.0.0.1:5000/items \
  -H "Content-Type: application/json" -d '{"name": "apple"}'
# {"id": 1, "name": "apple"}

curl -X POST http://127.0.0.1:5000/items \
  -H "Content-Type: application/json" -d '{"name": "banana"}'
# {"id": 2, "name": "banana"}

# 3. LIST (now has 2 items)
curl http://127.0.0.1:5000/items
# [{"id": 1, "name": "apple"}, {"id": 2, "name": "banana"}]

# 4. GET one item
curl http://127.0.0.1:5000/items/1
# {"id": 1, "name": "apple"}

# 5. UPDATE item 1
curl -X PUT http://127.0.0.1:5000/items/1 \
  -H "Content-Type: application/json" -d '{"name": "green apple"}'
# {"id": 1, "name": "green apple"}

# 6. DELETE item 2
curl -X DELETE http://127.0.0.1:5000/items/2
# (empty 204 response)

# 7. LIST again
curl http://127.0.0.1:5000/items
# [{"id": 1, "name": "green apple"}]

Quick Check ✅

You want to create a new item in an API. Which HTTP method and status code are correct?


🏆 Challenge 1: Notes API

Build a full Notes CRUD API (15–20 min)

A note has: id, title, body, created_at (datetime string).

  • GET /notes — list all notes
  • GET /notes/<int:id> — get one note (404 if not found)
  • POST /notes — create note (require title, optional body) → 201
  • PUT /notes/<int:id> — update title/body → 404 if missing
  • DELETE /notes/<int:id> — delete → 204
Hints Use from datetime import datetime then datetime.now().isoformat() for the timestamp.
Store notes in a list: notes = []
Bonus Add GET /notes?search=flask that filters notes whose title contains the search term.

Solution: Notes API

from flask import Flask, request, jsonify
from datetime import datetime

app = Flask(__name__)
notes = []
next_id = 1

def find(note_id):
    return next((n for n in notes if n["id"] == note_id), None)

@app.route("/notes", methods=["GET"])
def list_notes():
    search = request.args.get("search", "").lower()
    result = [n for n in notes if search in n["title"].lower()] if search else notes
    return jsonify(result)

@app.route("/notes/<int:note_id>", methods=["GET"])
def get_note(note_id):
    note = find(note_id)
    if not note:
        return jsonify({"error": "not found"}), 404
    return jsonify(note)

@app.route("/notes", methods=["POST"])
def create_note():
    global next_id
    body  = request.get_json(silent=True) or {}
    title = (body.get("title") or "").strip()
    if not title:
        return jsonify({"error": "title required"}), 400
    note = {"id": next_id, "title": title,
            "body": body.get("body", ""),
            "created_at": datetime.now().isoformat()}
    notes.append(note)
    next_id += 1
    return jsonify(note), 201

@app.route("/notes/<int:note_id>", methods=["PUT"])
def update_note(note_id):
    note = find(note_id)
    if not note:
        return jsonify({"error": "not found"}), 404
    body = request.get_json(silent=True) or {}
    if "title" in body:
        note["title"] = (body["title"] or "").strip() or note["title"]
    if "body" in body:
        note["body"] = body["body"]
    return jsonify(note)

@app.route("/notes/<int:note_id>", methods=["DELETE"])
def delete_note(note_id):
    note = find(note_id)
    if not note:
        return jsonify({"error": "not found"}), 404
    notes.remove(note)
    return "", 204

Custom Error Handlers

By default, Flask returns HTML error pages. For an API, return JSON instead.

  • Use @app.errorhandler(code) to intercept errors
  • Works for 404, 405, 500, and more
  • Keeps your API consistent — always returns JSON
Good practice Always return JSON error responses with a clear "error" key so client code can handle them consistently.
from flask import Flask, jsonify
app = Flask(__name__)

@app.errorhandler(404)
def not_found(e):
    return jsonify({"error": "Resource not found"}), 404

@app.errorhandler(405)
def method_not_allowed(e):
    return jsonify({"error": "Method not allowed"}), 405

@app.errorhandler(500)
def internal_error(e):
    return jsonify({"error": "Internal server error"}), 500

# Now ALL unmatched routes return JSON 404
# instead of Flask's default HTML page
@app.route("/items")
def items():
    return jsonify([])

if __name__ == "__main__":
    app.run(debug=True)

Quick Check ✅

A client sends a POST /items request but forgets to include the required "name" field. What status code should you return?


Common Beginner Mistakes

❌ Forgot to activate venv pip install Flask installs to system Python, not your project.
Always activate: source .venv/bin/activate
❌ Missing methods=["POST"] Flask returns 405 Method Not Allowed if you visit a POST route with GET (or vice versa).
❌ Forgot Content-Type header request.get_json() returns None if the header is missing. Always add -H "Content-Type: application/json" to curl.
❌ Not handling None from get_json() Always use request.get_json(silent=True) or {} to avoid crashes.
❌ debug=True in production Debug mode exposes your code. Set debug=False before deploying.
❌ Returning plain strings instead of JSON Use jsonify() so clients get proper JSON with correct headers.

What's Next?

Immediate next topics

  • Replace the list with a real database (SQLite + SQLAlchemy)
  • Use Flask Blueprints to organise large apps
  • Add authentication (tokens, JWT)
  • Test with pytest and Flask's test client

Resources

  • flask.palletsprojects.com — Official docs
  • Flask Quickstart guide (in the docs)
  • pip install flask-sqlalchemy for database support
  • Postman (GUI) or Thunder Client (VS Code extension) for testing
Well done! You can now build and test a complete REST API with Flask — GET, POST, PUT, DELETE, error handling, query params, and path params.