INFO 153B/253B: Backend Web Architecture
Week 6
Kay Ashaolu - Instructor
Suk Min Hwang - GSI
Quick check-in on O'Reilly Chapter 5
if checks, declare the rules once.
{"price": "expensive"}{"price": 9.99}# Without validation - what could go wrong?
@app.route('/items', methods=['POST'])
def create_item():
data = request.get_json()
# Hope for the best!
item = {"name": data["name"], "price": data["price"]}
items.append(item)
return jsonify(item), 201
| Term | Definition | Example |
|---|---|---|
| Schema | Blueprint for data structure | ItemSchema |
| Field | Single piece of data | fields.String() |
| load | Deserialize (JSON to Python) | Incoming request |
| dump | Serialize (Python to JSON) | Outgoing response |
from flask_smorest import Blueprint
blp = Blueprint("items", __name__, description="Operations on items")
@blp.route("/items")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True)) # Output
def get(self):
return items
@blp.arguments(ItemSchema) # Input
@blp.response(201, ItemSchema) # Output
def post(self, item_data):
# item_data is already validated!
items.append(item_data)
return item_data
@blp.arguments - validates incoming JSON@blp.response - formats outgoing JSON@blp.arguments, @blp.responseSecurity, integrity, and user experience
# No validation - user sends: {"price": "free"}
@app.route('/items', methods=['POST'])
def create_item():
data = request.get_json()
new_item = {
"name": data["name"], # KeyError if missing!
"price": data["price"] # "free" is not a number!
}
# Later, in another endpoint...
total = sum(item["price"] for item in items) # TypeError!
KeyError crash (500 error)@app.route('/items', methods=['POST'])
def create_item():
data = request.get_json()
# Manual validation - tedious and error-prone
if not data:
return {"error": "No data provided"}, 400
if "name" not in data:
return {"error": "Name is required"}, 400
if not isinstance(data["name"], str):
return {"error": "Name must be a string"}, 400
if len(data["name"]) > 100:
return {"error": "Name too long"}, 400
if "price" not in data:
return {"error": "Price is required"}, 400
if not isinstance(data["price"], (int, float)):
return {"error": "Price must be a number"}, 400
if data["price"] < 0:
return {"error": "Price cannot be negative"}, 400
# Finally, do the actual work...
new_item = {"name": data["name"], "price": data["price"]}
items.append(new_item)
return jsonify(new_item), 201
from marshmallow import Schema, fields, validate
class ItemSchema(Schema):
name = fields.Str(required=True, validate=validate.Length(max=100))
price = fields.Float(required=True, validate=validate.Range(min=0))
# In your route:
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
# item_data is already validated!
items.append(item_data)
return item_data
Schema definitions and field types
from marshmallow import Schema, fields
class ItemSchema(Schema):
id = fields.Int(dump_only=True) # Only in output
name = fields.Str(required=True) # Required in input
price = fields.Float(required=True)
description = fields.Str() # Optional
created_at = fields.DateTime(dump_only=True)
Str, Int, Float, Bool, DateTime| Field Type | Python Type | Example |
|---|---|---|
fields.Str() | str | "hello" |
fields.Int() | int | 42 |
fields.Float() | float | 3.14 |
fields.Bool() | bool | true |
fields.DateTime() | datetime | "2024-01-15T10:30:00" |
fields.List(fields.Str()) | list | ["a", "b", "c"] |
fields.Nested(OtherSchema) | dict | {"name": "..."} |
from marshmallow import Schema, fields, validate
class ItemSchema(Schema):
# Length constraints
name = fields.Str(
required=True,
validate=validate.Length(min=1, max=100)
)
# Range constraints
price = fields.Float(
required=True,
validate=validate.Range(min=0, max=10000)
)
# Choice from options
category = fields.Str(
validate=validate.OneOf(["electronics", "clothing", "food"])
)
# Regex pattern
sku = fields.Str(
validate=validate.Regexp(r'^[A-Z]{3}-\d{4}$')
)
from marshmallow import Schema, fields, validates, ValidationError
class ItemSchema(Schema):
name = fields.Str(required=True)
price = fields.Float(required=True)
discount_price = fields.Float()
@validates("name")
def validate_name(self, value):
"""Custom validation for name field."""
if value.lower() == "test":
raise ValidationError("Name cannot be 'test'")
if " " in value:
raise ValidationError("Name cannot have double spaces")
@validates("discount_price")
def validate_discount(self, value):
"""Discount must be less than regular price."""
# Note: cross-field validation is tricky here
# For complex cases, use @validates_schema
if value < 0:
raise ValidationError("Discount cannot be negative")
class ItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class StoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
# Nested items - one store has many items
items = fields.List(fields.Nested(ItemSchema), dump_only=True)
// JSON output:
{
"id": 1,
"name": "My Store",
"items": [
{"id": 1, "name": "Chair", "price": 49.99},
{"id": 2, "name": "Table", "price": 149.99}
]
}
# Only in OUTPUT
id = fields.Int(dump_only=True)
created_at = fields.DateTime(dump_only=True)
# Only in INPUT
password = fields.Str(load_only=True)
confirm_password = fields.Str(load_only=True)
dump_only for IDs and timestamps.
load_only for passwords and secrets.
Connecting schemas to Flask routes
from flask import Flask
from flask_smorest import Api
app = Flask(__name__)
# Required configuration
app.config["API_TITLE"] = "Stores API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
# Optional: Enable Swagger UI
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
# Register blueprints
api.register_blueprint(items_blp)
api.register_blueprint(stores_blp)
from flask_smorest import Blueprint
from flask.views import MethodView
# Create a blueprint for items
blp = Blueprint(
"items", # Blueprint name
__name__, # Import name
description="Operations on items" # For API docs
)
@blp.route("/items")
class ItemList(MethodView):
def get(self):
"""Get all items."""
pass
def post(self):
"""Create a new item."""
pass
@blp.route("/items/<int:item_id>")
class Item(MethodView):
def get(self, item_id):
"""Get a specific item."""
pass
@blp.route("/items")
class ItemList(MethodView):
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
"""Create a new item.
item_data is already validated!
- If invalid: 422 Unprocessable Entity (automatic)
- If valid: this function runs with clean data
"""
new_item = {
"id": len(items) + 1,
**item_data # Already a dict with correct types
}
items.append(new_item)
return new_item
ItemSchema@blp.route("/items")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
"""Get all items.
Returns a list of items.
many=True means "this returns a list, not a single object"
"""
return items # List of dicts
@blp.route("/items/<int:item_id>")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
"""Get a specific item."""
item = next((i for i in items if i["id"] == item_id), None)
if not item:
abort(404, message="Item not found")
return item # Single dict
many=True for lists, omit for single objects# schemas.py
from marshmallow import Schema, fields, validate
class ItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
price = fields.Float(required=True, validate=validate.Range(min=0))
class ItemUpdateSchema(Schema):
name = fields.Str(validate=validate.Length(min=1, max=100))
price = fields.Float(validate=validate.Range(min=0))
# resources/item.py
from flask_smorest import Blueprint, abort
from flask.views import MethodView
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("items", __name__, description="Operations on items")
items = []
@blp.route("/items")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return items
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = {"id": len(items) + 1, **item_data}
items.append(item)
return item
@blp.route("/items/<int:item_id>")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = next((i for i in items if i["id"] == item_id), None)
if not item:
abort(404, message="Item not found")
return item
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = next((i for i in items if i["id"] == item_id), None)
if not item:
abort(404, message="Item not found")
item.update(item_data)
return item
http://localhost:5000/swagger-uiUser-friendly validation errors
# Request with missing field
curl -X POST http://localhost:5000/items \
-H "Content-Type: application/json" \
-d '{"name": "Chair"}' # Missing price!
// Response: 422 Unprocessable Entity
{
"code": 422,
"errors": {
"json": {
"price": ["Missing data for required field."]
}
},
"status": "Unprocessable Entity"
}
errors.json contains field-by-field errors# Request with multiple issues
curl -X POST http://localhost:5000/items \
-H "Content-Type: application/json" \
-d '{"price": -10}' # Missing name AND negative price!
// Response: 422 Unprocessable Entity
{
"code": 422,
"errors": {
"json": {
"name": ["Missing data for required field."],
"price": ["Must be greater than or equal to 0."]
}
},
"status": "Unprocessable Entity"
}
from marshmallow import Schema, fields, validate
class ItemSchema(Schema):
name = fields.Str(
required=True,
validate=validate.Length(min=1, max=100),
error_messages={
"required": "Item name is required.",
"null": "Item name cannot be null.",
}
)
price = fields.Float(
required=True,
validate=validate.Range(
min=0,
error="Price must be a positive number."
),
error_messages={
"required": "Price is required.",
"invalid": "Price must be a valid number.",
}
)
error_messages dict for field-level messageserror parameter on validators for custom messagesfrom flask_smorest import abort
@blp.route("/items/<int:item_id>")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = next((i for i in items if i["id"] == item_id), None)
if not item:
abort(404, message="Item not found")
return item
def delete(self, item_id):
item = next((i for i in items if i["id"] == item_id), None)
if not item:
abort(404, message="Item not found")
if item.get("protected"):
abort(403, message="Cannot delete protected items")
items.remove(item)
return "", 204
abort() for non-validation errors (404, 403, etc.)message parameter for error detailsKey takeaways from today
| Week | Topic | Building On |
|---|---|---|
| 7 | SQLAlchemy + Migrations | Store validated data in PostgreSQL |
| 8 | Async Task Queues | Background processing with Celery |
| 9 | System Design | Scalability, reliability, caching |
Add validation to a Store API
ItemSchema and StoreSchema@blp.arguments and @blp.responseAdd Validation to an Items API
ItemSchema and StoreSchema (15 min)@blp.arguments decorators (10 min)@blp.response decorators (10 min)git clone <your-repo-url>
cd in-class-exploration-week-6-<your-username>
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
flask run
curl http://localhost:5000/items
curl -X POST http://localhost:5000/items \
-H "Content-Type: application/json" \
-d '{"anything": "works"}' # No validation!
# Valid request - should succeed
curl -X POST http://localhost:5000/items \
-H "Content-Type: application/json" \
-d '{"name": "Chair", "price": 49.99}'
# Invalid request - should return 422
curl -X POST http://localhost:5000/items \
-H "Content-Type: application/json" \
-d '{"name": "Chair"}' # Missing price!
# Check Swagger UI
open http://localhost:5000/swagger-ui
git add .
git commit -m "In-class exploration submission"
git push
Want to keep working? Continue after submitting - just push additional changes.
Website: groups.ischool.berkeley.edu/i253/sp26
Email: kay@ischool.berkeley.edu