Aller au contenu principal

11.2 — Building REST APIs with Flask

Theory 50 min Intermediate

REST Principles

REST (Representational State Transfer) is an architectural style for APIs built on HTTP. A RESTful API uses HTTP methods to perform CRUD operations on resources.

┌──────────────────────────────────────────────────────────────┐
│ REST API Design │
│ │
│ HTTP Method │ URL │ Action │
│ ────────────┼────────────────────┼──────────────────────── │
│ GET │ /api/products │ List all products │
│ POST │ /api/products │ Create a new product │
│ GET │ /api/products/42 │ Get product #42 │
│ PUT │ /api/products/42 │ Replace product #42 │
│ PATCH │ /api/products/42 │ Update fields of #42 │
│ DELETE │ /api/products/42 │ Delete product #42 │
└──────────────────────────────────────────────────────────────┘

HTTP Status Codes:

CodeMeaning
200OK — successful GET, PUT, PATCH
201Created — successful POST
204No Content — successful DELETE
400Bad Request — invalid input
401Unauthorized — not authenticated
403Forbidden — authenticated but not allowed
404Not Found
422Unprocessable Entity — validation error
500Internal Server Error

Full CRUD API Example

from flask import Flask, jsonify, request, abort

app = Flask(__name__)

# In-memory "database" (for learning; use a real DB in production)
products = {
1: {"id": 1, "name": "Laptop", "price": 999.99, "stock": 10},
2: {"id": 2, "name": "Keyboard", "price": 49.95, "stock": 50},
}
next_id = 3

# ── GET /api/products ─────────────────────────────────────────
@app.route("/api/products", methods=["GET"])
def list_products():
# Optional filtering: /api/products?min_price=100
min_price = request.args.get("min_price", 0, type=float)
result = [p for p in products.values() if p["price"] >= min_price]
return jsonify({"products": result, "count": len(result)})

# ── GET /api/products/<id> ────────────────────────────────────
@app.route("/api/products/<int:product_id>", methods=["GET"])
def get_product(product_id):
product = products.get(product_id)
if product is None:
abort(404) # triggers 404 error handler
return jsonify(product)

# ── POST /api/products ────────────────────────────────────────
@app.route("/api/products", methods=["POST"])
def create_product():
global next_id
data = request.get_json(silent=True) # returns None on parse failure

if not data:
return jsonify({"error": "Request body must be JSON"}), 400

# Validate required fields
errors = {}
if "name" not in data or not data["name"].strip():
errors["name"] = "Name is required"
if "price" not in data:
errors["price"] = "Price is required"
elif not isinstance(data["price"], (int, float)) or data["price"] <= 0:
errors["price"] = "Price must be a positive number"

if errors:
return jsonify({"errors": errors}), 422

product = {
"id": next_id,
"name": data["name"].strip(),
"price": float(data["price"]),
"stock": data.get("stock", 0),
}
products[next_id] = product
next_id += 1

return jsonify(product), 201 # 201 = Created

# ── PATCH /api/products/<id> ──────────────────────────────────
@app.route("/api/products/<int:product_id>", methods=["PATCH"])
def update_product(product_id):
product = products.get(product_id)
if product is None:
abort(404)

data = request.get_json(silent=True) or {}

# Only update fields that were sent
if "name" in data:
product["name"] = data["name"].strip()
if "price" in data:
if not isinstance(data["price"], (int, float)) or data["price"] <= 0:
return jsonify({"error": "Invalid price"}), 422
product["price"] = float(data["price"])
if "stock" in data:
product["stock"] = int(data["stock"])

return jsonify(product)

# ── DELETE /api/products/<id> ─────────────────────────────────
@app.route("/api/products/<int:product_id>", methods=["DELETE"])
def delete_product(product_id):
if product_id not in products:
abort(404)
del products[product_id]
return "", 204 # 204 = No Content

# ── Error handlers ────────────────────────────────────────────
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "Not found"}), 404

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

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

Testing the API

Use curl or a tool like HTTPie or Postman:

# Install HTTPie (easier than curl)
pip install httpie

# List products
http GET localhost:5000/api/products

# Create a product
http POST localhost:5000/api/products name="Monitor" price:=350.00 stock:=25

# Update stock
http PATCH localhost:5000/api/products/1 stock:=8

# Delete
http DELETE localhost:5000/api/products/2

Blueprints — Modular Organization

As your app grows, put related routes into Blueprints (Flask's module system):

project/
├── app.py ← create_app() factory
├── routes/
│ ├── __init__.py
│ ├── products.py ← Blueprint for /api/products
│ └── users.py ← Blueprint for /api/users

routes/products.py

from flask import Blueprint, jsonify, request, abort

bp = Blueprint("products", __name__, url_prefix="/api/products")

_db = {1: {"id":1, "name":"Laptop", "price":999.99}}

@bp.route("", methods=["GET"])
def list_products():
return jsonify(list(_db.values()))

@bp.route("/<int:pid>", methods=["GET"])
def get_product(pid):
p = _db.get(pid)
if not p:
abort(404)
return jsonify(p)

app.py — Application Factory

from flask import Flask

def create_app():
app = Flask(__name__)

# Register blueprints
from routes.products import bp as products_bp
from routes.users import bp as users_bp
app.register_blueprint(products_bp)
app.register_blueprint(users_bp)

return app

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

The application factory pattern makes the app easy to test and configure for different environments.


Request Validation with Marshmallow

For production APIs, use a dedicated validation library:

pip install marshmallow
from marshmallow import Schema, fields, ValidationError, validate

class ProductSchema(Schema):
name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
price = fields.Float(required=True, validate=validate.Range(min=0.01))
stock = fields.Int(load_default=0, validate=validate.Range(min=0))

product_schema = ProductSchema()

@app.route("/api/products", methods=["POST"])
def create_product():
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "JSON required"}), 400

try:
validated = product_schema.load(data)
except ValidationError as err:
return jsonify({"errors": err.messages}), 422

# validated is a dict with only validated fields
return jsonify(validated), 201

Flask-CORS — Cross-Origin Requests

When a browser-based frontend (e.g., React) calls your Flask API, you need CORS headers:

pip install flask-cors
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app) # Allow all origins (dev only)
# CORS(app, origins=["https://myfrontend.com"]) # Production: restrict origins

API Versioning

Version your API to allow breaking changes without breaking existing clients:

# Strategy 1: URL prefix
# /api/v1/products, /api/v2/products

from flask import Blueprint

v1 = Blueprint("v1", __name__, url_prefix="/api/v1")
v2 = Blueprint("v2", __name__, url_prefix="/api/v2")

@v1.route("/products")
def v1_products():
return jsonify([{"id": 1, "name": "Laptop"}]) # old format

@v2.route("/products")
def v2_products():
return jsonify({
"data": [{"id": 1, "name": "Laptop", "slug": "laptop"}],
"meta": {"version": "2.0"}
}) # new format

app.register_blueprint(v1)
app.register_blueprint(v2)

Key Vocabulary

TermDefinition
RESTArchitectural style using HTTP methods + URLs to represent resource operations
CRUDCreate, Read, Update, Delete — the 4 basic data operations
BlueprintFlask's module system for organizing routes
Application factorycreate_app() function for flexible app configuration
MarshmallowPython library for data serialization and validation
CORSCross-Origin Resource Sharing — allows browsers to call APIs on other domains
abort(code)Immediately stops request processing and returns an error response
get_json(silent=True)Parses JSON body; returns None if malformed (instead of raising)

Summary

  • Use HTTP methods (GET/POST/PUT/PATCH/DELETE) correctly and return proper status codes
  • request.get_json() parses JSON request bodies
  • jsonify({...}) serializes Python dicts to JSON responses
  • abort(404) triggers the registered error handler
  • Blueprints organize routes by feature area; the app factory wires them together
  • Validate input data before processing — use Marshmallow for complex validation
  • Add CORS headers when a browser-based frontend calls your API

Next: 11.3 — Flask with a Database (SQLAlchemy) to persist data in a real database.