API REST in Python: fondamenti e primo endpoint
Questo articolo apre una serie di sette puntate dedicate alla costruzione di API REST con Python. Partiremo dalle fondamenta, con la creazione di un progetto e il primo endpoint JSON, e arriveremo progressivamente a un'applicazione completa dotata di persistenza, trasformazione dei dati, validazione, autenticazione e test automatici. Il filo conduttore sarà sempre lo stesso: un catalogo di libri, che faremo crescere articolo dopo articolo. In questa prima puntata ci concentriamo sui concetti architetturali di REST, sul motivo per cui FastAPI è uno strumento eccellente per esporre API e sulla creazione del primo endpoint funzionante.
La serie usa FastAPI con Pydantic 2 e presuppone Python 3.11 o superiore. FastAPI è oggi il framework di riferimento per costruire API in Python: sfrutta le annotazioni di tipo del linguaggio per generare validazione e documentazione in modo automatico, ed è progettato attorno al protocollo HTTP anziché attorno al rendering di pagine, il che lo rende un'ottima base per un servizio REST.
Che cosa significa REST
REST, acronimo di Representational State Transfer, non è un protocollo né una libreria, ma uno stile architetturale definito da Roy Fielding nel 2000. Un'API che aderisce a questo stile espone delle risorse identificate da URL e le manipola attraverso i metodi del protocollo HTTP. L'idea centrale è che il server non conserva lo stato della conversazione con il client: ogni richiesta contiene tutte le informazioni necessarie per essere elaborata. Questa proprietà, detta stateless, rende le API REST facili da scalare orizzontalmente, perché qualsiasi istanza del server è in grado di rispondere a qualsiasi richiesta.
Una risorsa è un'entità concettuale del dominio applicativo: nel nostro caso, un libro. Ogni risorsa è raggiungibile tramite un identificatore univoco, l'URL, e le operazioni su di essa vengono espresse dai metodi HTTP. La corrispondenza tipica tra i metodi e le operazioni CRUD (Create, Read, Update, Delete) è la seguente:
- Il metodo GET recupera una rappresentazione della risorsa senza modificarla.
- Il metodo POST crea una nuova risorsa all'interno di una collezione.
- Il metodo PUT sostituisce integralmente una risorsa esistente.
- Il metodo PATCH applica una modifica parziale a una risorsa.
- Il metodo DELETE rimuove la risorsa.
A ogni richiesta il server risponde con un codice di stato numerico che ne comunica l'esito. I codici nella famiglia 2xx indicano successo, quelli 4xx segnalano un errore imputabile al client (per esempio una risorsa inesistente o dati malformati), mentre quelli 5xx indicano un problema interno del server. Rispettare la semantica di metodi e codici di stato è ciò che distingue un'API realmente RESTful da un semplice insieme di endpoint HTTP.
Perché FastAPI per le API REST
FastAPI nasce con un obiettivo preciso: costruire API in modo rapido e sicuro sfruttando ciò che Python già offre, ovvero le annotazioni di tipo. La conseguenza è che moltissimo del lavoro ripetitivo è risolto dal framework a partire dalla semplice firma delle funzioni. Dichiarando che un parametro è un intero o che il corpo della richiesta ha una certa forma, otteniamo gratuitamente la validazione dell'input, la conversione dei tipi, la serializzazione JSON dell'output e, cosa notevole, una documentazione interattiva sempre aggiornata.
Tre caratteristiche lo rendono particolarmente adatto alle API REST. La prima è l'integrazione con Pydantic, la libreria che valida i dati sulla base dei tipi dichiarati: sarà il cuore della nostra validazione e della nostra serializzazione. La seconda è il supporto nativo per il codice asincrono, che permette di gestire un gran numero di connessioni concorrenti senza bloccare il server durante le operazioni di rete o di database. La terza è la generazione automatica di uno schema OpenAPI, dal quale FastAPI ricava una documentazione navigabile senza che dobbiamo scrivere una sola riga in più.
Creare il progetto
Assumiamo di avere già a disposizione Python 3.11 o superiore. La buona pratica consolidata in Python è isolare le dipendenze di ogni progetto in un ambiente virtuale. Creiamo una cartella per il progetto, al suo interno un ambiente virtuale e lo attiviamo.
mkdir catalogo-libri
cd catalogo-libri
python -m venv .venv
source .venv/bin/activate
Su Windows il comando di attivazione è invece .venv\Scripts\activate. Con l'ambiente attivo, installiamo FastAPI con le sue dipendenze consigliate, che includono il server ASGI Uvicorn e lo strumento a riga di comando.
pip install "fastapi[standard]"
La variante [standard] installa tutto ciò che serve per lo sviluppo, compreso il comando fastapi che useremo per avviare il server con ricaricamento automatico.
Il primo endpoint
Cominciamo con l'endpoint più semplice e utile che esista: un health check, ovvero una rotta che conferma che il servizio è attivo. Creiamo un file main.py nella radice del progetto.
from fastapi import FastAPI
# L'istanza dell'applicazione: è l'oggetto su cui registriamo le rotte
app = FastAPI()
@app.get("/health")
def health() -> dict:
# FastAPI serializza automaticamente il dizionario in una risposta JSON
return {"status": "ok", "service": "catalogo-libri"}
Il decoratore @app.get("/health") registra la funzione come gestore delle richieste GET verso il percorso /health. In FastAPI queste funzioni si chiamano path operation function: sono il corrispettivo delle azioni di un controller. Avviamo il server di sviluppo.
fastapi dev main.py
Il server si mette in ascolto su http://127.0.0.1:8000 e si ricarica automaticamente a ogni modifica del codice. Interroghiamo l'endpoint dal terminale con curl.
curl -i http://127.0.0.1:8000/health
La risposta conterrà l'intestazione Content-Type: application/json, il codice di stato 200 e il corpo {"status":"ok","service":"catalogo-libri"}. Non abbiamo scritto nulla per serializzare il dizionario né per impostare l'intestazione: FastAPI si è occupato di tutto, deducendo il comportamento dal valore restituito dalla funzione.
La documentazione interattiva
Prima ancora di aggiungere altre rotte, FastAPI ci offre un regalo. Apriamo nel browser l'indirizzo http://127.0.0.1:8000/docs: troveremo un'interfaccia interattiva, generata da Swagger UI, che elenca tutti gli endpoint, ne descrive i parametri e permette di provarli direttamente dalla pagina. All'indirizzo /redoc è disponibile una seconda resa della stessa documentazione. Entrambe derivano dallo schema OpenAPI che il framework costruisce automaticamente a partire dalle firme delle nostre funzioni, ed è raggiungibile in formato grezzo su /openapi.json.
Questo è uno dei tratti distintivi di FastAPI: la documentazione non è un artefatto separato da mantenere allineato al codice, ma una conseguenza diretta di come il codice è scritto. Man mano che arricchiremo i nostri endpoint con tipi e modelli, la documentazione si arricchirà di pari passo, senza sforzo aggiuntivo.
Dai file singoli a una struttura di progetto
Definire tutte le rotte in un unico main.py è comodo per un esempio, ma non scala. In un'applicazione reale conviene organizzare il codice in un pacchetto e suddividere le rotte in moduli tematici, chiamati router. Un router è l'equivalente concettuale di un gruppo di rotte: raccoglie endpoint affini e viene poi montato nell'applicazione principale. Predisponiamo questa struttura.
catalogo-libri/
├── app/
│ ├── __init__.py
│ ├── main.py
│ └── routers/
│ ├── __init__.py
│ └── health.py
└── .venv/
Spostiamo l'endpoint di health check in un router dedicato, nel file app/routers/health.py, arricchendolo con un timestamp.
from datetime import datetime, timezone
from fastapi import APIRouter
# Il router raccoglie le rotte relative allo stato del servizio
router = APIRouter()
@router.get("/health")
def health() -> dict:
# Restituisce lo stato di salute del servizio insieme all'ora corrente
return {
"status": "ok",
"service": "catalogo-libri",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
Nel file app/main.py creiamo l'applicazione e vi montiamo il router, scegliendo di servire tutte le rotte sotto il prefisso /api, come è convenzione per le API.
from fastapi import FastAPI
from app.routers import health
# Il titolo comparirà nella documentazione automatica
app = FastAPI(title="Catalogo Libri API")
# Monta le rotte del router health sotto il prefisso /api
app.include_router(health.router, prefix="/api")
Ora avviamo il server puntando al modulo dell'applicazione all'interno del pacchetto.
fastapi dev app/main.py
L'endpoint è raggiungibile su http://127.0.0.1:8000/api/health. Il comportamento visibile è identico a prima, ma la struttura è molto migliore: ogni router descrive un insieme coerente di rotte, e l'applicazione principale si limita ad assemblarli. Questa separazione diventa essenziale man mano che l'API cresce, ed è esattamente lo schema su cui, nella prossima puntata, costruiremo la risorsa libro.
Tipi di ritorno e serializzazione
Nell'esempio abbiamo annotato la funzione con -> dict, ma restituire un dizionario grezzo è una soluzione provvisoria. FastAPI converte automaticamente in JSON i tipi Python di base — dizionari, liste, stringhe, numeri, valori booleani — e sa gestire anche tipi più ricchi come le date, serializzandole in formato ISO 8601. Il vero salto di qualità, però, avviene quando dichiariamo la forma della risposta con un modello Pydantic: da quel momento FastAPI non solo serializza, ma valida e documenta anche la struttura dei dati in uscita.
Vedremo i modelli in dettaglio nella puntata sulla trasformazione dei dati. Per ora è utile sapere che il codice di stato di una risposta si controlla con il parametro status_code del decoratore, oppure restituendo un oggetto Response costruito a mano. Il metodo store di una risorsa, per esempio, dovrà restituire un 201, come vedremo tra poco.
from fastapi import status
@router.get("/health", status_code=status.HTTP_200_OK)
def health() -> dict:
return {"status": "ok"}
Usare la costante status.HTTP_200_OK anziché il numero 200 rende il codice più leggibile e meno esposto a errori di battitura. Adotteremo questa convenzione per tutti i codici di stato lungo la serie.
Conclusione
In questa prima puntata abbiamo posto le fondamenta. Abbiamo chiarito che cosa significhi REST, perché lo stile stateless favorisca la scalabilità e come FastAPI, sfruttando le annotazioni di tipo di Python, ci permetta di esporre API con pochissima cerimonia e con una documentazione interattiva gratuita. Abbiamo creato il progetto in un ambiente virtuale, scritto un primo endpoint di health check, esplorato la documentazione automatica e spostato la logica in un router dedicato all'interno di una struttura di progetto ordinata.
Nella prossima puntata entreremo nel vivo del nostro dominio: definiremo le rotte per la risorsa libro sfruttando un router dedicato e i parametri di percorso, e implementeremo tutte le azioni CRUD rispettando la semantica dei metodi HTTP e dei codici di stato. Getteremo così l'impalcatura su cui, nelle puntate successive, innesteremo la persistenza con un database.