Python Tutorial / Build a Flask 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.

JSON representation versus persistent storage build on the rules demonstrated in Convert and Save JSON Data.

Save the complete API application

from flask import Flask, jsonify, request

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 4096
expenses = []

@app.errorhandler(413)
def too_large(error):
    return jsonify(error="request body too large"), 413

@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
    for payload in [{"name":"x","amount":True}, {"name":"x","amount":-1}, {"name":" ","amount":1}, [1,2]]:
        response = client.post("/api/expenses", json=payload)
        assert response.status_code == 400 and response.is_json
    assert client.post("/api/expenses", data="{", content_type="application/json").status_code == 400
    too_large = client.post("/api/expenses", data="x"*5000, content_type="application/json")
    assert too_large.status_code == 413 and too_large.is_json
    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"])
print("validation and 413 checks passed")

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.

Requests exceeding MAX_CONTENT_LENGTH return 413; Flask’s default error page can be HTML. The dedicated handler above makes this error JSON. Malformed JSON, arrays, boolean amounts, negatives and blank names return 400 without storing a row. The expanded test ends with validation and 413 checks passed. This contract covers the application routes and handled errors, not requests rejected earlier by a web server or proxy.

Official behavior and conditions: Flask: handling application errors · Flask: MAX_CONTENT_LENGTH