Python Tutorial / Build a JSON API Server and Validate Input
A small Flask API maps HTTP requests to Python functions. Check the JSON object, required fields, types and ranges, and use status codes to distinguish a successful creation from invalid input.
Define the local-development scope
This API is a same-computer exercise. Its in-memory list disappears when the process exits and is not shared across independent server processes. A public service needs persistent storage, authentication, authorization, deployment and concurrency design.
Install Flask in the project environment:
python -m pip install flask
Save the application as app.py and run python app.py. If port 5000 is occupied, change the port and client address together.
For the relevant APIs and platform requirements, see Build a JSON API Server and Validate Input documentation.
Save the complete API application
from flask import Flask, jsonify, request
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 4096
expenses = []
@app.get("/api/expenses")
def list_expenses():
return jsonify(expenses)
@app.post("/api/expenses")
def add_expense():
data = request.get_json(silent=True)
if not isinstance(data, dict):
return jsonify(error="expected JSON object"), 400
name, amount = data.get("name"), data.get("amount")
if not isinstance(name, str) or not name.strip() or type(amount) is not int or amount < 0:
return jsonify(error="invalid name or amount"), 400
row = {"id": len(expenses) + 1, "name": name.strip(), "amount": amount}
expenses.append(row)
return jsonify(row), 201
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False)
GET /api/expenses returns the list; POST creates one record. Success returns 201 and a validation error returns 400. MAX_CONTENT_LENGTH limits request-body size; it does not impose every resource limit an application may need.
Exercise the routes with Flask’s test client
Save test_client.py alongside app.py:
from app import app, expenses
expenses.clear()
with app.test_client() as client:
created = client.post("/api/expenses", json={"name":"lunch", "amount":8000})
assert created.status_code == 201
assert client.post("/api/expenses", json={"name":"x", "amount":True}).status_code == 400
rows = client.get("/api/expenses")
assert rows.status_code == 200 and len(rows.get_json()) == 1
print(created.status_code, rows.get_json()[0]["amount"])
Run:
python test_client.py
Expect 201 8000. The test client calls Flask’s request-handling flow without a separately running server. It does not verify the real network, reverse proxy or deployment environment.
Check data policy and application lifetime
JSON true becomes bool, and isinstance(True, int) is true. The exact-type check excludes it from integer amounts. Invalid JSON becomes None through silent=True and receives the defined validation error.
Keep the development server local and do not expose an interactive debugger in a public deployment. Stop it with Ctrl+C and observe that restarting clears the list. Durable transaction records require storage and transaction rules, as introduced in the SQLite lesson.
The example’s IDs are derived from one process’s list length. They are suitable for this controlled demonstration, not a substitute for identifiers and synchronization in a concurrent production service.




