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:
| Code | Meaning |
|---|---|
| 200 | OK — successful GET, PUT, PATCH |
| 201 | Created — successful POST |
| 204 | No Content — successful DELETE |
| 400 | Bad Request — invalid input |
| 401 | Unauthorized — not authenticated |
| 403 | Forbidden — authenticated but not allowed |
| 404 | Not Found |
| 422 | Unprocessable Entity — validation error |
| 500 | Internal 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
| Term | Definition |
|---|---|
| REST | Architectural style using HTTP methods + URLs to represent resource operations |
| CRUD | Create, Read, Update, Delete — the 4 basic data operations |
| Blueprint | Flask's module system for organizing routes |
| Application factory | create_app() function for flexible app configuration |
| Marshmallow | Python library for data serialization and validation |
| CORS | Cross-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 bodiesjsonify({...})serializes Python dicts to JSON responsesabort(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.