Corso Python / Creare una API JSON con Flask e validare gli ingressi

Con Flask puoi collegare una richiesta HTTP a una funzione Python e restituire una risposta JSON. In una piccola API, verifica forma dell’oggetto, campi, tipi e valori prima di memorizzare un record e usa codici di stato distinti per creazione riuscita ed errore.

Definire l’esercizio locale e installare Flask

Questa applicazione è un esercizio sullo stesso computer. Le spese vivono in una lista in memoria: terminando il processo si perdono e due processi indipendenti non condividono la lista. La rappresentazione JSON dei dati non equivale alla loro conservazione persistente.

Installa il pacchetto esterno Flask nell’ambiente virtuale del progetto:

python -m pip install flask

Salva il programma come app.py, evitando il nome flask.py che nasconderebbe il pacchetto. La guida introduttiva ufficiale di Flask descrive applicazione e route. Un servizio pubblico richiede ulteriori scelte per archiviazione, autenticazione, autorizzazioni, distribuzione e concorrenza.

Creare l’applicazione API completa

In una cartella dedicata salva questo file UTF-8 come app.py:

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)

Dal terminale nella cartella, con l’ambiente selezionato, puoi eseguire python app.py. Il server di sviluppo ascolta su 127.0.0.1:5000, con debug disattivato. Se la porta è già occupata, cambia nel codice il valore di port e usa lo stesso numero nell’indirizzo del client, senza fermare programmi estranei.

Una richiesta GET /api/expenses restituisce la lista con stato 200. Una richiesta POST allo stesso percorso deve contenere un oggetto JSON con un nome testuale non vuoto e un importo intero non negativo. Per esempio, {"name":"lunch","amount":8000} crea un record e restituisce stato 201. Gli spazi esterni del nome vengono eliminati prima del salvataggio.

Un nome vuoto, un importo negativo, un booleano o un dato di tipo errato producono 400 senza aggiungere una riga. request.get_json(silent=True) restituisce None se il corpo non è un JSON leggibile con il tipo di contenuto richiesto: il controllo iniziale lo rifiuta come oggetto non valido.

MAX_CONTENT_LENGTH=4096 limita i byte del corpo della richiesta. Non limita il numero complessivo di record, tutte le risorse del processo o la durata delle operazioni. Quando viene superato, il gestore dedicato restituisce un errore JSON con stato 413. Il codice non stabilisce invece un importo massimo positivo: è una regola ulteriore da progettare se serve.

Provare le route con il client di test

Salva il seguente file completo come test_client.py, accanto a 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")

Esegui da quella cartella:

python test_client.py

Non avviare un server separato per questo test. app.test_client() attraversa la gestione delle richieste di Flask all’interno del processo Python, senza verificare una connessione di rete o un proxy. Il file importato non avvia il server grazie al controllo __name__.

L’uscita attesa ha due righe: 201 8000 e validation and 413 checks passed. Il test crea una spesa valida, prova gli errori indicati e verifica che resti soltanto una riga. expenses.clear() azzera esclusivamente la lista di quel processo di test: non modifica la memoria di un eventuale server avviato in un altro terminale.

Esegui Python senza -O, perché il test usa istruzioni assert che verrebbero eliminate dall’ottimizzazione. Se una verifica fallisce, controlla la risposta e la regola interessata; non cambiare lo stato atteso per nascondere un errore. Un’esecuzione del test terminata correttamente non dimostra che siano corretti configurazione di rete o ambiente di distribuzione.

Capire tipi JSON, errori e durata dei dati

Il valore JSON true diventa un bool Python, e isinstance(True, int) è vero. Il confronto esatto type(amount) is int impedisce quindi di trattare un booleano come una spesa intera. Zero resta valido, mentre una stringa numerica o un numero in virgola mobile vengono rifiutati.

Il contratto JSON riguarda le route e gli errori gestiti qui. Non tutti gli errori del server diventano automaticamente JSON: un percorso inesistente o un metodo non ammesso possono ancora usare la risposta HTML predefinita. Un proxy può rifiutare una richiesta prima di Flask. La guida alla gestione degli errori Flask e la configurazione di MAX_CONTENT_LENGTH chiariscono questi confini.

Gli ID derivano da len(expenses) + 1 e sono adatti alla dimostrazione controllata, non sostituiscono identificatori e sincronizzazione per un’applicazione concorrente. POST ripetute creano righe ripetute; non è presente una deduplicazione basata sul contenuto.

Se hai avviato il server locale, termina il suo terminale con Ctrl+C al termine della prova. Una nuova esecuzione ricrea la lista vuota. Per dati durevoli collega un archivio e definisci le transazioni, come nella lezione su SQLite e salvataggio dei dati. Il server di sviluppo e il debugger non vanno usati per una distribuzione pubblica.