Flask in Python - GET, POST, PUT, DELETE with curl Testing #39
Video: Flask in Python - GET, POST, PUT, DELETE with curl Testing #39 by Taught by Celeste AI - AI Coding Coach
Python Flask: Lightweight Web Framework
pip install flask.app = Flask(__name__).@app.route("/path")registers an endpoint.request.argsfor query,request.get_json()for body.jsonify(...)for JSON response. Microframework — no built-in ORM, validation, or templates beyond basics. You compose what you need.
Flask is the classic Python web framework. Where FastAPI emphasizes types and async, Flask emphasizes simplicity and flexibility — minimal core, choose-your-own-adventure for the rest.
Hello world
from flask import Flask
app = Flask(__name__)
@app.route("/")
def root():
return "Hello, World!"
if __name__ == "__main__":
app.run(debug=True, port=5000)
Run:
pip install flask
python app.py
# or
flask run
Open http://localhost:5000/.
@app.route("/") registers the function as a handler for that path. Default method is GET.
Returning JSON
from flask import jsonify
@app.route("/users")
def list_users():
return jsonify(users)
@app.route("/")
def root():
return jsonify({"message": "Welcome", "version": "1.0"})
jsonify(...) serializes a dict (or list) to JSON and sets Content-Type: application/json.
In recent Flask versions, returning a dict directly works too:
@app.route("/users")
def list_users():
return {"users": users} # auto-jsonified
Path parameters
@app.route("/users/<int:user_id>")
def get_user(user_id):
if user_id in users:
return jsonify(users[user_id])
return jsonify({"error": "User not found"}), 404
<int:user_id> is a converter — captures an integer and passes it as the function argument. Available converters:
<string:name>(default) — anything except/.<int:n>— integer.<float:f>— float.<path:p>— like string but allows/.<uuid:id>— UUID.
Tuple (body, status_code) returns a custom status. (body, 404) for not found.
Query parameters
from flask import request
@app.route("/search")
def search():
q = request.args.get("q", "")
limit = request.args.get("limit", 5, type=int)
results = [u for u in users.values() if q.lower() in u["name"].lower()]
return jsonify({"query": q, "limit": limit, "results": results[:limit]})
request.args is a dict-like object of query parameters. .get(key, default, type=...) provides a default and converts.
Request body (POST)
@app.route("/items", methods=["POST"])
def create_item():
data = request.get_json()
if data is None or "name" not in data:
return jsonify({"error": "name is required"}), 400
item = {
"id": next_id(),
"name": data["name"],
"price": data.get("price", 0),
}
items.append(item)
return jsonify(item), 201
methods=["POST"] overrides the default GET. request.get_json() parses the JSON body — returns None if the body isn't JSON.
For form-encoded POSTs (HTML forms), use request.form.
For files, request.files.
All HTTP methods on one route
@app.route("/items/<int:item_id>", methods=["GET", "PUT", "DELETE"])
def item(item_id):
if request.method == "GET":
return jsonify(items[item_id])
elif request.method == "PUT":
items[item_id] = request.get_json()
return jsonify(items[item_id])
elif request.method == "DELETE":
del items[item_id]
return "", 204
Or split into separate functions:
@app.route("/items/<int:item_id>", methods=["GET"])
def get_item(item_id): ...
@app.route("/items/<int:item_id>", methods=["PUT"])
def update_item(item_id): ...
@app.route("/items/<int:item_id>", methods=["DELETE"])
def delete_item(item_id): ...
Either pattern works. Separate functions are easier to test.
Error handling
from flask import abort
@app.route("/items/<int:item_id>")
def get_item(item_id):
if item_id not in items:
abort(404, description="Item not found")
return jsonify(items[item_id])
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": str(error.description)}), 404
@app.errorhandler(500)
def server_error(error):
return jsonify({"error": "Internal server error"}), 500
abort(code, description=...) raises an HTTPException. Register handlers with @app.errorhandler(code).
Templates (Jinja2)
from flask import render_template
@app.route("/users/<int:user_id>")
def user_page(user_id):
user = users[user_id]
return render_template("user.html", user=user)
templates/user.html:
<h1>{{ user.name }}</h1>
<p>Email: {{ user.email }}</p>
{% for tag in user.tags %}
<span>{{ tag }}</span>
{% endfor %}
Flask uses Jinja2 templates by default. {{ }} for expressions, {% %} for control flow.
For pure JSON APIs, you don't need templates. They're for HTML pages.
Sessions and cookies
from flask import session
app.secret_key = "your-secret-key" # required for sessions
@app.route("/login", methods=["POST"])
def login():
username = request.form["username"]
session["user"] = username
return redirect(url_for("dashboard"))
@app.route("/dashboard")
def dashboard():
user = session.get("user")
if not user:
return redirect(url_for("login_page"))
return f"Hello, {user}"
session is a dict-like proxy that's stored client-side as a signed cookie. Don't put sensitive data in there — it's just signed, not encrypted (use flask-session for server-side storage).
Blueprints: organizing routes
For larger apps, split into blueprints:
# users.py
from flask import Blueprint
users_bp = Blueprint("users", __name__, url_prefix="/users")
@users_bp.route("/")
def list_users(): ...
@users_bp.route("/<int:user_id>")
def get_user(user_id): ...
# app.py
from users import users_bp
app = Flask(__name__)
app.register_blueprint(users_bp)
Each blueprint is a mini-app. Use one per resource or feature.
Extensions
Flask is intentionally minimal. Common extensions:
- Flask-SQLAlchemy — SQLAlchemy ORM.
- Flask-Migrate — Alembic migrations.
- Flask-Login — user sessions.
- Flask-WTF — forms with CSRF.
- Flask-Limiter — rate limiting.
- Flask-Cors — CORS.
Pick what you need; ignore the rest.
A complete task API
from flask import Flask, jsonify, request
app = Flask(__name__)
tasks = {}
next_id = 1
@app.route("/tasks", methods=["POST"])
def create_task():
global next_id
data = request.get_json()
if not data or "title" not in data:
return jsonify({"error": "title is required"}), 400
task = {
"id": next_id,
"title": data["title"],
"status": "pending",
"priority": data.get("priority", "medium"),
}
tasks[next_id] = task
next_id += 1
return jsonify(task), 201
@app.route("/tasks", methods=["GET"])
def list_tasks():
status = request.args.get("status")
results = list(tasks.values())
if status:
results = [t for t in results if t["status"] == status]
return jsonify({"tasks": results, "count": len(results)})
@app.route("/tasks/<int:task_id>", methods=["GET"])
def get_task(task_id):
task = tasks.get(task_id)
if not task:
return jsonify({"error": "Not found"}), 404
return jsonify(task)
@app.route("/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
if task_id not in tasks:
return jsonify({"error": "Not found"}), 404
data = request.get_json()
for key in ["title", "status", "priority"]:
if key in data:
tasks[task_id][key] = data[key]
return jsonify(tasks[task_id])
@app.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
if task_id not in tasks:
return jsonify({"error": "Not found"}), 404
removed = tasks.pop(task_id)
return jsonify(removed)
if __name__ == "__main__":
app.run(debug=True)
CRUD on tasks. ~50 lines. Clear and direct.
Testing
def test_create_task():
client = app.test_client()
response = client.post("/tasks", json={"title": "Buy milk"})
assert response.status_code == 201
assert response.json["title"] == "Buy milk"
Flask's test client runs the app in-process. Use it with pytest.
Deployment
# Don't use the built-in `flask run` for production
# Use a real WSGI server:
pip install gunicorn
gunicorn -w 4 app:app
# Or behind nginx, systemd, Docker, etc.
The flask run command is for dev only — single-threaded, no auto-restart on workers.
Flask vs FastAPI
| Aspect | Flask | FastAPI |
|---|---|---|
| Sync/async | Sync first; async added in 2.0+ | Async-first |
| Type hints | Optional | Core feature |
| Validation | Manual / extensions | Built-in via Pydantic |
| Auto-docs | No (extensions) | Yes (/docs) |
| Speed | Slower | Faster |
| Ecosystem | Huge, mature | Newer but rich |
| Learning curve | Gentler | Type-hint-aware |
| Best for | Small/medium apps, traditional web | APIs, microservices |
For a JSON API today, FastAPI is usually the better default. For a small server-rendered web app, Flask is simpler and has more templates/examples.
Common stumbles
Forgetting methods=. Default is GET only. POST/PUT/DELETE need explicit declaration.
Returning a tuple wrong. return jsonify(data), 201 is correct. return (jsonify(data), 201) works too. But return data, 201 (raw dict) only works in newer Flask.
request.get_json() returns None. When the request didn't send JSON, or the content-type is wrong. Always check.
Globals for state. tasks = {} doesn't work with multiple workers — each has its own copy. Use a database or shared store.
flask run in production. Single-threaded, slow. Use gunicorn (or uwsgi).
Mutating request.args. It's immutable. To modify, copy first.
Forgetting secret_key for sessions. Sessions silently break.
Long-running endpoints. Block the worker. Use Celery, RQ, or background workers for heavy tasks.
What's next
Lesson 40: packaging with pyproject.toml. Building distributable Python packages.
Recap
@app.route("/path") registers an endpoint. <int:name> for typed path parameters. request.args for query, request.get_json() for body. jsonify(...) for JSON responses; tuple (body, status) for custom codes. methods=["GET", "POST"] for HTTP method routing. Use blueprints to organize larger apps. For sessions, set app.secret_key. Run via gunicorn in production, not the dev server.
Next lesson: pyproject.toml packaging.